mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-04 18:12:23 +02:00
Show users communities on about page
This commit is contained in:
parent
7379b0aa9b
commit
0414039aa1
5
.changeset/clean-moons-arrive.md
Normal file
5
.changeset/clean-moons-arrive.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show users joined communities on about page
|
@ -69,6 +69,7 @@ import RelaysView from "./views/relays";
|
||||
import RelayView from "./views/relays/relay";
|
||||
import RelayReviewsView from "./views/relays/reviews";
|
||||
import PopularRelaysView from "./views/relays/popular";
|
||||
import UserDMsTab from "./views/user/dms";
|
||||
const UserTracksTab = lazy(() => import("./views/user/tracks"));
|
||||
|
||||
const ToolsHomeView = lazy(() => import("./views/tools"));
|
||||
@ -200,6 +201,7 @@ const router = createHashRouter([
|
||||
{ path: "relays", element: <UserRelaysTab /> },
|
||||
{ path: "reports", element: <UserReportsTab /> },
|
||||
{ path: "muted-by", element: <MutedByView /> },
|
||||
{ path: "dms", element: <UserDMsTab /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ import RawPre from "./raw-pre";
|
||||
|
||||
export default function NoteDebugModal({ event, ...props }: { event: NostrEvent } & Omit<ModalProps, "children">) {
|
||||
return (
|
||||
<Modal {...props}>
|
||||
<Modal size="6xl" {...props}>
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalCloseButton />
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Box, Card, CardBody, CardHeader, CardProps, Flex, Link, Text } from "@chakra-ui/react";
|
||||
import { Box, Button, Card, CardBody, CardHeader, CardProps, Flex, Link, Text, useDisclosure } from "@chakra-ui/react";
|
||||
|
||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import { NostrEvent } from "../../../types/nostr-event";
|
||||
@ -11,8 +11,11 @@ import { useMemo } from "react";
|
||||
import { embedEmoji, embedNostrHashtags, embedNostrLinks, embedNostrMentions } from "../../embed-types";
|
||||
import { EmbedableContent } from "../../../helpers/embeds";
|
||||
import Timestamp from "../../timestamp";
|
||||
import { CodeIcon } from "../../icons";
|
||||
import NoteDebugModal from "../../debug-modals/note-debug-modal";
|
||||
|
||||
export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
|
||||
const debugModal = useDisclosure();
|
||||
const address = getSharableEventAddress(event);
|
||||
|
||||
const content = useMemo(() => {
|
||||
@ -26,24 +29,30 @@ export default function EmbeddedUnknown({ event, ...props }: Omit<CardProps, "ch
|
||||
}, [event.content]);
|
||||
|
||||
return (
|
||||
<Card {...props}>
|
||||
<CardHeader display="flex" gap="2" alignItems="center" p="2" pb="0" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="md" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<Link ml="auto" href={address ? buildAppSelectUrl(address) : ""} isExternal>
|
||||
<Timestamp timestamp={event.created_at} />
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
<Flex gap="2">
|
||||
<Text>Kind: {event.kind}</Text>
|
||||
<Link href={address ? buildAppSelectUrl(address) : ""} isExternal color="blue.500">
|
||||
{address && truncatedId(address)}
|
||||
<>
|
||||
<Card {...props}>
|
||||
<CardHeader display="flex" gap="2" alignItems="center" p="2" pb="0" flexWrap="wrap">
|
||||
<UserAvatarLink pubkey={event.pubkey} size="xs" />
|
||||
<UserLink pubkey={event.pubkey} isTruncated fontWeight="bold" fontSize="md" />
|
||||
<UserDnsIdentityIcon pubkey={event.pubkey} onlyIcon />
|
||||
<Link ml="auto" href={address ? buildAppSelectUrl(address) : ""} isExternal>
|
||||
<Timestamp timestamp={event.created_at} />
|
||||
</Link>
|
||||
</Flex>
|
||||
<Box whiteSpace="pre-wrap">{content}</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</CardHeader>
|
||||
<CardBody p="2">
|
||||
<Flex gap="2">
|
||||
<Text>Kind: {event.kind}</Text>
|
||||
<Link href={address ? buildAppSelectUrl(address) : ""} isExternal color="blue.500">
|
||||
{address && truncatedId(address)}
|
||||
</Link>
|
||||
<Button leftIcon={<CodeIcon />} ml="auto" size="sm" variant="outline" onClick={debugModal.onOpen}>
|
||||
View Raw
|
||||
</Button>
|
||||
</Flex>
|
||||
<Box whiteSpace="pre-wrap">{content}</Box>
|
||||
</CardBody>
|
||||
</Card>
|
||||
{debugModal.isOpen && <NoteDebugModal isOpen={debugModal.isOpen} onClose={debugModal.onClose} event={event} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
ToolsIcon,
|
||||
LogoutIcon,
|
||||
NotesIcon,
|
||||
LightningIcon,
|
||||
} from "../icons";
|
||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||
import accountService from "../../services/account";
|
||||
@ -194,6 +195,9 @@ export default function NavItems() {
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
<Button leftIcon={<LightningIcon boxSize={6} color="yellow.400" />} {...buttonProps}>
|
||||
Donate
|
||||
</Button>
|
||||
{account && (
|
||||
<Button onClick={() => accountService.logout()} leftIcon={<LogoutIcon boxSize={6} />} {...buttonProps}>
|
||||
Logout
|
||||
|
@ -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 useSubscribedCommunitiesList from "../../../hooks/use-subscribed-communities-list";
|
||||
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-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 } = useSubscribedCommunitiesList(account?.pubkey);
|
||||
const { pointers } = useJoinedCommunitiesList(account?.pubkey);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const repost = async (communityPointer?: AddressPointer) => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { forwardRef } from "react";
|
||||
import { Select, SelectProps } from "@chakra-ui/react";
|
||||
|
||||
import useSubscribedCommunitiesList from "../../hooks/use-subscribed-communities-list";
|
||||
import useJoinedCommunitiesList from "../../hooks/use-communities-joined-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 } = useSubscribedCommunitiesList(account?.pubkey);
|
||||
const { pointers } = useJoinedCommunitiesList(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 useSubscribedCommunitiesList(pubkey?: string, opts?: RequestOptions) {
|
||||
export default function useJoinedCommunitiesList(pubkey?: string, opts?: RequestOptions) {
|
||||
const account = useCurrentAccount();
|
||||
const key = pubkey ?? account?.pubkey;
|
||||
|
@ -3,7 +3,7 @@ import dayjs from "dayjs";
|
||||
import { Button, ButtonProps, useToast } from "@chakra-ui/react";
|
||||
|
||||
import { DraftNostrEvent, NostrEvent } from "../../../types/nostr-event";
|
||||
import useSubscribedCommunitiesList from "../../../hooks/use-subscribed-communities-list";
|
||||
import useJoinedCommunitiesList from "../../../hooks/use-communities-joined-list";
|
||||
import { useCurrentAccount } from "../../../hooks/use-current-account";
|
||||
import { SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER, getCommunityName } from "../../../helpers/nostr/communities";
|
||||
import { NOTE_LIST_KIND, listAddCoordinate, listRemoveCoordinate } from "../../../helpers/nostr/lists";
|
||||
@ -17,7 +17,7 @@ export default function CommunityJoinButton({
|
||||
...props
|
||||
}: Omit<ButtonProps, "children"> & { community: NostrEvent }) {
|
||||
const account = useCurrentAccount();
|
||||
const { list, pointers } = useSubscribedCommunitiesList(account?.pubkey);
|
||||
const { list, pointers } = useJoinedCommunitiesList(account?.pubkey);
|
||||
const { requestSignature } = useSigningContext();
|
||||
const toast = useToast();
|
||||
|
||||
|
@ -25,7 +25,7 @@ import dayjs from "dayjs";
|
||||
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { ErrorBoundary } from "../../components/error-boundary";
|
||||
import useSubscribedCommunitiesList from "../../hooks/use-subscribed-communities-list";
|
||||
import useJoinedCommunitiesList from "../../hooks/use-communities-joined-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,7 @@ function CommunitiesHomePage() {
|
||||
const createModal = useDisclosure();
|
||||
|
||||
const readRelays = useReadRelayUrls();
|
||||
const { pointers: communityCoordinates } = useSubscribedCommunitiesList(account.pubkey, { alwaysRequest: true });
|
||||
const { pointers: communityCoordinates } = useJoinedCommunitiesList(account.pubkey, { alwaysRequest: true });
|
||||
const communities = useReplaceableEvents(communityCoordinates, readRelays).sort(
|
||||
(a, b) => b.created_at - a.created_at,
|
||||
);
|
||||
|
@ -12,6 +12,7 @@ import {
|
||||
IconButton,
|
||||
Image,
|
||||
Link,
|
||||
SimpleGrid,
|
||||
Stat,
|
||||
StatGroup,
|
||||
StatHelpText,
|
||||
@ -55,6 +56,10 @@ 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";
|
||||
|
||||
function buildDescriptionContent(description: string) {
|
||||
let content: EmbedableContent = [description.trim()];
|
||||
@ -65,19 +70,162 @@ 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 };
|
||||
const contextRelays = useAdditionalRelayContext();
|
||||
|
||||
const metadata = useUserMetadata(pubkey, contextRelays);
|
||||
const contacts = useUserContactList(pubkey, contextRelays);
|
||||
const npub = nip19.npubEncode(pubkey);
|
||||
const nprofile = useSharableProfileId(pubkey);
|
||||
|
||||
const { value: stats } = useAsync(() => trustedUserStatsService.getUserStats(pubkey), [pubkey]);
|
||||
const followerCount = useEventCount({ "#p": [pubkey], kinds: [Kind.Contacts] });
|
||||
|
||||
const aboutContent = metadata?.about && buildDescriptionContent(metadata?.about);
|
||||
const parsedNip05 = metadata?.nip05 ? parseAddress(metadata.nip05) : undefined;
|
||||
|
||||
@ -185,116 +333,9 @@ export default function UserAboutTab() {
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<UserProfileBadges pubkey={pubkey} />
|
||||
<UserProfileBadges pubkey={pubkey} px="2" />
|
||||
<UserStatsAccordion pubkey={pubkey} />
|
||||
|
||||
<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>
|
||||
<Flex gap="2" wrap="wrap">
|
||||
<Button
|
||||
as={Link}
|
||||
@ -324,6 +365,8 @@ export default function UserAboutTab() {
|
||||
Nostree page
|
||||
</Button>
|
||||
</Flex>
|
||||
|
||||
<UserJoinedCommunities pubkey={pubkey} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||
import UserAvatarLink from "../../../components/user-avatar-link";
|
||||
import { UserLink } from "../../../components/user-link";
|
||||
import Timestamp from "../../../components/timestamp";
|
||||
import { useState } from "react";
|
||||
|
||||
function Badge({ pubkey, badge, award }: { pubkey: string; badge: NostrEvent; award: NostrEvent }) {
|
||||
const naddr = getSharableEventAddress(badge);
|
||||
@ -81,14 +82,20 @@ function Badge({ pubkey, badge, award }: { pubkey: string; badge: NostrEvent; aw
|
||||
|
||||
export default function UserProfileBadges({ pubkey, ...props }: Omit<FlexProps, "children"> & { pubkey: string }) {
|
||||
const profileBadges = useUserProfileBadges(pubkey);
|
||||
const [limit, setLimit] = useState<number | null>(10);
|
||||
|
||||
if (profileBadges.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Flex gap="2" wrap="wrap" {...props}>
|
||||
{profileBadges.map(({ badge, award }) => (
|
||||
<Flex gap="2" wrap="wrap" alignItems="center" {...props}>
|
||||
{(limit !== null ? profileBadges.slice(0, limit) : profileBadges).map(({ badge, award }) => (
|
||||
<Badge key={getEventCoordinate(badge)} pubkey={pubkey} badge={badge} award={award} />
|
||||
))}
|
||||
{limit !== null && profileBadges.length > limit && (
|
||||
<Button variant="outline" onClick={() => setLimit(null)}>
|
||||
Show More
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Flex } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
@ -27,7 +26,7 @@ export default function UserArticlesTab() {
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
{articles.map((article) => (
|
||||
<EmbeddedArticle article={article} />
|
||||
<EmbeddedArticle key={article.id} article={article} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</VerticalPageLayout>
|
||||
|
83
src/views/user/dms.tsx
Normal file
83
src/views/user/dms.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import { Flex, Text } from "@chakra-ui/react";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider from "../../providers/intersection-observer";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
|
||||
import { NostrEvent, isPTag } from "../../types/nostr-event";
|
||||
import UserAvatarLink from "../../components/user-avatar-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import ArrowRight from "../../components/icons/arrow-right";
|
||||
import { AtIcon } from "../../components/icons";
|
||||
import Timestamp from "../../components/timestamp";
|
||||
import ArrowLeft from "../../components/icons/arrow-left";
|
||||
|
||||
function DirectMessage({ dm, pubkey }: { dm: NostrEvent; pubkey: string }) {
|
||||
const sender = dm.pubkey;
|
||||
const receiver = dm.tags.find(isPTag)?.[1];
|
||||
|
||||
if (sender === pubkey) {
|
||||
if (!receiver) return null;
|
||||
return (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<ArrowRight boxSize={6} />
|
||||
<Timestamp timestamp={dm.created_at} />
|
||||
<Text>Sent: </Text>
|
||||
<UserAvatarLink pubkey={receiver} size="sm" />
|
||||
<UserLink pubkey={receiver} fontWeight="bold" fontSize="lg" />
|
||||
</Flex>
|
||||
);
|
||||
} else if (receiver === pubkey) {
|
||||
return (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<ArrowLeft boxSize={6} />
|
||||
<Timestamp timestamp={dm.created_at} />
|
||||
<Text>Received: </Text>
|
||||
<UserAvatarLink pubkey={sender} size="sm" />
|
||||
<UserLink pubkey={sender} fontWeight="bold" fontSize="lg" />
|
||||
</Flex>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Flex gap="2" alignItems="center">
|
||||
<AtIcon boxSize={6} />
|
||||
<Timestamp timestamp={dm.created_at} />
|
||||
<Text>Mentioned: </Text>
|
||||
<UserAvatarLink pubkey={pubkey} size="sm" />
|
||||
<UserLink pubkey={pubkey} fontWeight="bold" fontSize="lg" />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function UserDMsTab() {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const readRelays = useAdditionalRelayContext();
|
||||
|
||||
const timeline = useTimelineLoader(pubkey + "-articles", readRelays, [
|
||||
{
|
||||
authors: [pubkey],
|
||||
kinds: [Kind.EncryptedDirectMessage],
|
||||
},
|
||||
{ "#p": [pubkey], kinds: [Kind.EncryptedDirectMessage] },
|
||||
]);
|
||||
|
||||
const dms = useSubject(timeline.timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
<VerticalPageLayout>
|
||||
{dms.map((dm) => (
|
||||
<DirectMessage key={dm.id} dm={dm} pubkey={pubkey} />
|
||||
))}
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</VerticalPageLayout>
|
||||
</IntersectionObserverProvider>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user