diff --git a/.changeset/plenty-laws-rule.md b/.changeset/plenty-laws-rule.md new file mode 100644 index 000000000..f08c0f3e3 --- /dev/null +++ b/.changeset/plenty-laws-rule.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Show community members diff --git a/src/components/note/components/quote-repost-button.tsx b/src/components/note/components/quote-repost-button.tsx index 71b2d4cea..44be32878 100644 --- a/src/components/note/components/quote-repost-button.tsx +++ b/src/components/note/components/quote-repost-button.tsx @@ -20,7 +20,7 @@ export function QuoteRepostButton({ const handleClick = () => { const nevent = getSharableEventAddress(event); - openModal({ initContent: "\nnostr:" + nevent }); + openModal({ cacheFormKey: null, initContent: "\nnostr:" + nevent }); }; return ( diff --git a/src/components/post-modal/index.tsx b/src/components/post-modal/index.tsx index 6dcaf89d3..2c2db88ea 100644 --- a/src/components/post-modal/index.tsx +++ b/src/components/post-modal/index.tsx @@ -56,7 +56,7 @@ type FormValues = { }; export type PostModalProps = { - cacheFormKey?: string; + cacheFormKey?: string | null; initContent?: string; initCommunity?: string; }; diff --git a/src/hooks/use-cache-form.ts b/src/hooks/use-cache-form.ts index 1c2af705b..1fef48141 100644 --- a/src/hooks/use-cache-form.ts +++ b/src/hooks/use-cache-form.ts @@ -4,14 +4,15 @@ import { useMount, useUnmount } from "react-use"; // TODO: make these caches expire export default function useCacheForm( - key: string, + key: string | null, getValues: UseFormGetValues, setValue: UseFormSetValue, state: UseFormStateReturn, ) { - 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>(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]); } diff --git a/src/hooks/use-count-community-members.ts b/src/hooks/use-count-community-members.ts new file mode 100644 index 000000000..a61672df3 --- /dev/null +++ b/src/hooks/use-count-community-members.ts @@ -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], + }); +} diff --git a/src/services/event-count.ts b/src/services/event-count.ts index ca33d7eb5..57cc730d9 100644 --- a/src/services/event-count.ts +++ b/src/services/event-count.ts @@ -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>(() => new Subject()); diff --git a/src/views/communities/components/community-card.tsx b/src/views/communities/components/community-card.tsx index 15f67a6a1..a211ed84e 100644 --- a/src/views/communities/components/community-card.tsx +++ b/src/views/communities/components/community-card.tsx @@ -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 & { community: NostrEvent }) { const ref = useRef(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 & { by + {countMembers && ( + + + {readablizeSats(countMembers)} + + )} + {/* {notesInLastMonth !== undefined && {notesInLastMonth} Posts in the past month} */} diff --git a/src/views/community/components/community-members-modal.tsx b/src/views/community/components/community-members-modal.tsx new file mode 100644 index 000000000..af2a8b8fc --- /dev/null +++ b/src/views/community/components/community-members-modal.tsx @@ -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 ( + + + + + + + + ); +} + +export default function ({ community, onClose, ...props }: Omit & { 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 ( + + + + + + Community Members + + + + + {lists.map((list) => ( + + ))} + + + + + + + + + + + ); +} diff --git a/src/views/community/components/horizonal-community-details.tsx b/src/views/community/components/horizonal-community-details.tsx index 86bbc85a4..b9daf3ab7 100644 --- a/src/views/community/components/horizonal-community-details.tsx +++ b/src/views/community/components/horizonal-community-details.tsx @@ -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 & { 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 ( - - - - - - - {description && ( - <> - - Description - - - - )} - - {more.isOpen ? ( - - - - Mods + <> + + + + + + + {description && ( + <> + + Description - - {mods.map((pubkey) => ( - - - - - ))} - - - {rules && ( + + + )} + + {more.isOpen ? ( + - - Rules - - {rules} - - )} - {communityRelays.length > 0 && ( - - - Relays + + Mods - + {mods.map((pubkey) => ( + + + + + ))} - )} - - ) : ( - - )} - - + + + Members + + {countMembers ? readablizeSats(countMembers) : "unknown"} + + {rules && ( + + + Rules + + {rules} + + )} + {communityRelays.length > 0 && ( + + + Relays + + + + + + )} + + ) : ( + + )} + + + {membersModal.isOpen && ( + + )} + ); } diff --git a/src/views/community/components/vertical-community-details.tsx b/src/views/community/components/vertical-community-details.tsx index 7e549b58b..f9a6437db 100644 --- a/src/views/community/components/vertical-community-details.tsx +++ b/src/views/community/components/vertical-community-details.tsx @@ -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 & { 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 ( - - {description && ( - <> - - About - - - - )} - - - - - - Mods - - - {mods.map((pubkey) => ( - - - - - ))} - - {rules && ( - <> - - Rules - - {rules} - - )} - {communityRelays.length > 0 && ( - <> - - Relays + <> + + {description && ( + + + About + + + + )} + + + + + + + Mods - + {mods.map((pubkey) => ( + + + + + ))} - + + + + Members + + {countMembers ? readablizeSats(countMembers) : "unknown"} + + {rules && ( + + + Rules + + {rules} + + )} + {communityRelays.length > 0 && ( + + + Relays + + + + + + )} + + {membersModal.isOpen && ( + )} - + ); }