show community members

fix quote repost cache issue
This commit is contained in:
hzrd149 2023-10-20 08:20:26 -05:00
parent ae3ef94c17
commit d7e289a65d
10 changed files with 243 additions and 95 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show community members

View File

@ -20,7 +20,7 @@ export function QuoteRepostButton({
const handleClick = () => {
const nevent = getSharableEventAddress(event);
openModal({ initContent: "\nnostr:" + nevent });
openModal({ cacheFormKey: null, initContent: "\nnostr:" + nevent });
};
return (

View File

@ -56,7 +56,7 @@ type FormValues = {
};
export type PostModalProps = {
cacheFormKey?: string;
cacheFormKey?: string | null;
initContent?: string;
initCommunity?: string;
};

View File

@ -4,14 +4,15 @@ import { useMount, useUnmount } from "react-use";
// TODO: make these caches expire
export default function useCacheForm<TFieldValues extends FieldValues = FieldValues>(
key: string,
key: string | null,
getValues: UseFormGetValues<TFieldValues>,
setValue: UseFormSetValue<TFieldValues>,
state: UseFormStateReturn<TFieldValues>,
) {
const storageKey = key + "-form-values";
const storageKey = key && key + "-form-values";
useMount(() => {
if (storageKey === null) return;
try {
const cached = localStorage.getItem(storageKey);
localStorage.removeItem(storageKey);
@ -29,12 +30,14 @@ export default function useCacheForm<TFieldValues extends FieldValues = FieldVal
const stateRef = useRef<UseFormStateReturn<TFieldValues>>(state);
stateRef.current = state;
useUnmount(() => {
if (storageKey === null) return;
if (stateRef.current.isDirty && !stateRef.current.isSubmitted) {
localStorage.setItem(storageKey, JSON.stringify(getValues()));
} else localStorage.removeItem(storageKey);
});
return useCallback(() => {
if (storageKey === null) return;
localStorage.removeItem(storageKey);
}, [storageKey]);
}

View File

@ -0,0 +1,13 @@
import { SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER } from "../helpers/nostr/communities";
import { getEventCoordinate } from "../helpers/nostr/events";
import { NOTE_LIST_KIND } from "../helpers/nostr/lists";
import { NostrEvent } from "../types/nostr-event";
import useEventCount from "./use-event-count";
export default function useCountCommunityMembers(community: NostrEvent) {
return useEventCount({
"#a": [getEventCoordinate(community)],
"#d": [SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER],
kinds: [NOTE_LIST_KIND],
});
}

View File

@ -9,7 +9,7 @@ import relayPoolService from "./relay-pool";
// TODO: move this to settings
const COUNT_RELAY = "wss://relay.nostr.band";
const RATE_LIMIT = 10 / 1000;
const RATE_LIMIT = 1000;
class EventCountService {
subjects = new SuperMap<string, Subject<number>>(() => new Subject<number>());

View File

@ -10,6 +10,9 @@ import {
Heading,
LinkBox,
LinkOverlay,
Tag,
TagLabel,
TagLeftIcon,
Text,
} from "@chakra-ui/react";
@ -21,12 +24,17 @@ import CommunityDescription from "./community-description";
import useCountCommunityPosts from "../hooks/use-count-community-post";
import UserAvatarLink from "../../../components/user-avatar-link";
import { UserLink } from "../../../components/user-link";
import useCountCommunityMembers from "../../../hooks/use-count-community-members";
import { readablizeSats } from "../../../helpers/bolt11";
import { CommunityIcon } from "../../../components/icons";
import User01 from "../../../components/icons/user-01";
function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & { community: NostrEvent }) {
const ref = useRef<HTMLDivElement | null>(null);
useRegisterIntersectionEntity(ref, getEventUID(community));
const name = getCommunityName(community);
const countMembers = useCountCommunityMembers(community);
// NOTE: disabled because nostr.band has a rate limit
// const notesInLastMonth = useCountCommunityPosts(community);
@ -60,6 +68,13 @@ function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & {
<UserAvatarLink pubkey={community.pubkey} size="sm" />
<Text>by</Text>
<UserLink pubkey={community.pubkey} />
{countMembers && (
<Tag variant="solid" ml="auto" alignSelf="flex-end" textShadow="none">
<TagLeftIcon as={User01} boxSize={4} />
<TagLabel>{readablizeSats(countMembers)}</TagLabel>
</Tag>
)}
{/* {notesInLastMonth !== undefined && <Text ml="auto">{notesInLastMonth} Posts in the past month</Text>} */}
</CardFooter>
</Card>

View File

@ -0,0 +1,77 @@
import {
Button,
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
ModalProps,
SimpleGrid,
} from "@chakra-ui/react";
import { NostrEvent } from "../../../types/nostr-event";
import useTimelineLoader from "../../../hooks/use-timeline-loader";
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
import { SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER, getCommunityRelays } from "../../../helpers/nostr/communities";
import { getEventCoordinate } from "../../../helpers/nostr/events";
import { NOTE_LIST_KIND } from "../../../helpers/nostr/lists";
import IntersectionObserverProvider from "../../../providers/intersection-observer";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
import { UserLink } from "../../../components/user-link";
import { UserDnsIdentityIcon } from "../../../components/user-dns-identity-icon";
import UserAvatarLink from "../../../components/user-avatar-link";
function UserCard({ pubkey }: { pubkey: string }) {
return (
<Flex overflow="hidden" gap="4" alignItems="center">
<UserAvatarLink pubkey={pubkey} />
<Flex direction="column" flex={1} overflow="hidden">
<UserLink pubkey={pubkey} fontWeight="bold" />
<UserDnsIdentityIcon pubkey={pubkey} />
</Flex>
</Flex>
);
}
export default function ({ community, onClose, ...props }: Omit<ModalProps, "children"> & { community: NostrEvent }) {
const communityCoordinate = getEventCoordinate(community);
const readRelays = useReadRelayUrls(getCommunityRelays(community));
const timeline = useTimelineLoader(`${communityCoordinate}-members`, readRelays, {
"#a": [communityCoordinate],
"#d": [SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER],
kinds: [NOTE_LIST_KIND],
});
const lists = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
return (
<IntersectionObserverProvider callback={callback}>
<Modal onClose={onClose} size="4xl" {...props}>
<ModalOverlay />
<ModalContent>
<ModalHeader px="4" pt="4" pb="0">
Community Members
</ModalHeader>
<ModalCloseButton />
<ModalBody p="4">
<SimpleGrid columns={{ base: 1, lg: 2 }} spacing="4">
{lists.map((list) => (
<UserCard key={list.id} pubkey={list.pubkey} />
))}
</SimpleGrid>
<TimelineActionAndStatus timeline={timeline} />
</ModalBody>
<ModalFooter px="4" pt="0" pb="4">
<Button onClick={onClose}>Close</Button>
</ModalFooter>
</ModalContent>
</Modal>
</IntersectionObserverProvider>
);
}

View File

@ -24,74 +24,90 @@ import { RelayIconStack } from "../../../components/relay-icon-stack";
import { NostrEvent } from "../../../types/nostr-event";
import CommunityJoinButton from "../../communities/components/community-subscribe-button";
import CommunityMenu from "./community-menu";
import useCountCommunityMembers from "../../../hooks/use-count-community-members";
import { readablizeSats } from "../../../helpers/bolt11";
import CommunityMembersModal from "./community-members-modal";
export default function HorizontalCommunityDetails({
community,
...props
}: Omit<CardProps, "children"> & { community: NostrEvent }) {
const membersModal = useDisclosure();
const communityRelays = getCommunityRelays(community);
const mods = getCommunityMods(community);
const description = getCommunityDescription(community);
const rules = getCommunityRules(community);
const more = useDisclosure();
const countMembers = useCountCommunityMembers(community);
return (
<Card {...props}>
<CardBody>
<ButtonGroup float="right">
<CommunityJoinButton community={community} />
<CommunityMenu community={community} aria-label="More" />
</ButtonGroup>
{description && (
<>
<Heading size="sm" mb="2">
Description
</Heading>
<CommunityDescription community={community} mb="2" />
</>
)}
{more.isOpen ? (
<SimpleGrid spacing="4" columns={2}>
<Box>
<Heading size="sm" mb="2">
Mods
<>
<Card {...props}>
<CardBody>
<ButtonGroup float="right">
<CommunityJoinButton community={community} />
<CommunityMenu community={community} aria-label="More" />
</ButtonGroup>
{description && (
<>
<Heading size="sm" mb="1">
Description
</Heading>
<Flex direction="column" gap="2">
{mods.map((pubkey) => (
<Flex gap="2">
<UserAvatarLink pubkey={pubkey} size="xs" />
<UserLink pubkey={pubkey} />
</Flex>
))}
</Flex>
</Box>
{rules && (
<CommunityDescription community={community} mb="1" />
</>
)}
{more.isOpen ? (
<SimpleGrid spacing="4" columns={2}>
<Box>
<Heading size="sm" mb="2">
Rules
</Heading>
<Text whiteSpace="pre-wrap">{rules}</Text>
</Box>
)}
{communityRelays.length > 0 && (
<Box>
<Heading size="sm" mt="4" mb="2">
Relays
<Heading size="sm" mb="1">
Mods
</Heading>
<Flex direction="column" gap="2">
<RelayIconStack relays={communityRelays} />
{mods.map((pubkey) => (
<Flex gap="2">
<UserAvatarLink pubkey={pubkey} size="xs" />
<UserLink pubkey={pubkey} />
</Flex>
))}
</Flex>
</Box>
)}
</SimpleGrid>
) : (
<Button variant="link" onClick={more.onOpen} w="full">
Show more
</Button>
)}
</CardBody>
</Card>
<Box as="button" textAlign="start" cursor="pointer" onClick={membersModal.onOpen}>
<Heading size="sm" mb="1">
Members
</Heading>
<Text>{countMembers ? readablizeSats(countMembers) : "unknown"}</Text>
</Box>
{rules && (
<Box>
<Heading size="sm" mb="1">
Rules
</Heading>
<Text whiteSpace="pre-wrap">{rules}</Text>
</Box>
)}
{communityRelays.length > 0 && (
<Box>
<Heading size="sm" mt="4" mb="1">
Relays
</Heading>
<Flex direction="column" gap="2">
<RelayIconStack relays={communityRelays} />
</Flex>
</Box>
)}
</SimpleGrid>
) : (
<Button variant="link" onClick={more.onOpen} w="full">
Show more
</Button>
)}
</CardBody>
</Card>
{membersModal.isOpen && (
<CommunityMembersModal isOpen={membersModal.isOpen} onClose={membersModal.onClose} community={community} />
)}
</>
);
}

View File

@ -1,4 +1,4 @@
import { Box, ButtonGroup, Card, CardProps, Flex, Heading, Text } from "@chakra-ui/react";
import { Box, ButtonGroup, Card, CardProps, Flex, Heading, Text, useDisclosure } from "@chakra-ui/react";
import {
getCommunityDescription,
getCommunityMods,
@ -12,59 +12,78 @@ import { RelayIconStack } from "../../../components/relay-icon-stack";
import { NostrEvent } from "../../../types/nostr-event";
import CommunityJoinButton from "../../communities/components/community-subscribe-button";
import CommunityMenu from "./community-menu";
import useCountCommunityMembers from "../../../hooks/use-count-community-members";
import CommunityMembersModal from "./community-members-modal";
import { readablizeSats } from "../../../helpers/bolt11";
export default function VerticalCommunityDetails({
community,
...props
}: Omit<CardProps, "children"> & { community: NostrEvent }) {
const membersModal = useDisclosure();
const communityRelays = getCommunityRelays(community);
const mods = getCommunityMods(community);
const description = getCommunityDescription(community);
const rules = getCommunityRules(community);
const countMembers = useCountCommunityMembers(community);
return (
<Card p="4" {...props}>
{description && (
<>
<Heading size="sm" mb="2">
About
</Heading>
<CommunityDescription community={community} maxLength={256} showExpand />
</>
)}
<ButtonGroup w="full" my="2">
<CommunityJoinButton community={community} flex={1} />
<CommunityMenu community={community} aria-label="More" />
</ButtonGroup>
<Heading size="sm" mt="4" mb="2">
Mods
</Heading>
<Flex direction="column" gap="2">
{mods.map((pubkey) => (
<Flex gap="2">
<UserAvatarLink pubkey={pubkey} size="xs" />
<UserLink pubkey={pubkey} />
</Flex>
))}
</Flex>
{rules && (
<>
<Heading size="sm" mt="4" mb="2">
Rules
</Heading>
<Text whiteSpace="pre-wrap">{rules}</Text>
</>
)}
{communityRelays.length > 0 && (
<>
<Heading size="sm" mt="4" mb="2">
Relays
<>
<Card p="4" gap="4" {...props}>
{description && (
<Box>
<Heading size="sm" mb="1">
About
</Heading>
<CommunityDescription community={community} maxLength={256} showExpand />
</Box>
)}
<ButtonGroup w="full">
<CommunityJoinButton community={community} flex={1} />
<CommunityMenu community={community} aria-label="More" />
</ButtonGroup>
<Box>
<Heading size="sm" mb="1">
Mods
</Heading>
<Flex direction="column" gap="2">
<RelayIconStack relays={communityRelays} />
{mods.map((pubkey) => (
<Flex gap="2">
<UserAvatarLink pubkey={pubkey} size="xs" />
<UserLink pubkey={pubkey} />
</Flex>
))}
</Flex>
</>
</Box>
<Box as="button" textAlign="start" cursor="pointer" onClick={membersModal.onOpen}>
<Heading size="sm" mb="1">
Members
</Heading>
<Text>{countMembers ? readablizeSats(countMembers) : "unknown"}</Text>
</Box>
{rules && (
<Box>
<Heading size="sm" mb="1">
Rules
</Heading>
<Text whiteSpace="pre-wrap">{rules}</Text>
</Box>
)}
{communityRelays.length > 0 && (
<Box>
<Heading size="sm" mb="1">
Relays
</Heading>
<Flex direction="column" gap="2">
<RelayIconStack relays={communityRelays} />
</Flex>
</Box>
)}
</Card>
{membersModal.isOpen && (
<CommunityMembersModal isOpen={membersModal.isOpen} onClose={membersModal.onClose} community={community} />
)}
</Card>
</>
);
}