improve community explore view

This commit is contained in:
hzrd149
2023-10-20 11:02:33 -05:00
parent 65d2858d14
commit 1e93c7bc23
3 changed files with 96 additions and 64 deletions

View File

@@ -1,9 +1,8 @@
import { memo, useRef } from "react"; import { memo, useRef } from "react";
import { Kind, nip19 } from "nostr-tools"; import { nip19 } from "nostr-tools";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import { import {
Card, Card,
CardBody,
CardFooter, CardFooter,
CardHeader, CardHeader,
CardProps, CardProps,
@@ -20,14 +19,13 @@ import { NostrEvent } from "../../../types/nostr-event";
import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer"; import { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
import { getEventUID } from "../../../helpers/nostr/events"; import { getEventUID } from "../../../helpers/nostr/events";
import { getCommunityImage, getCommunityName } from "../../../helpers/nostr/communities"; import { getCommunityImage, getCommunityName } from "../../../helpers/nostr/communities";
import CommunityDescription from "./community-description";
import useCountCommunityPosts from "../hooks/use-count-community-post";
import UserAvatarLink from "../../../components/user-avatar-link"; import UserAvatarLink from "../../../components/user-avatar-link";
import { UserLink } from "../../../components/user-link"; import { UserLink } from "../../../components/user-link";
import useCountCommunityMembers from "../../../hooks/use-count-community-members"; import useCountCommunityMembers from "../../../hooks/use-count-community-members";
import { readablizeSats } from "../../../helpers/bolt11"; import { readablizeSats } from "../../../helpers/bolt11";
import { CommunityIcon } from "../../../components/icons";
import User01 from "../../../components/icons/user-01"; import User01 from "../../../components/icons/user-01";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & { community: NostrEvent }) { function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & { community: NostrEvent }) {
const ref = useRef<HTMLDivElement | null>(null); const ref = useRef<HTMLDivElement | null>(null);
@@ -81,4 +79,10 @@ function CommunityCard({ community, ...props }: Omit<CardProps, "children"> & {
); );
} }
export function PointerCommunityCard({ pointer, ...props }: Omit<CardProps, "children"> & { pointer: AddressPointer }) {
const community = useReplaceableEvent(pointer);
if (!community) return <span>Loading {pointer.identifier}</span>;
return <CommunityCard community={community} {...props} />;
}
export default memo(CommunityCard); export default memo(CommunityCard);

View File

@@ -1,69 +1,107 @@
import { useCallback, useMemo } from "react"; import { useMemo } from "react";
import { Flex, SimpleGrid } from "@chakra-ui/react"; import { AvatarGroup, Button, Flex, Switch, useDisclosure } from "@chakra-ui/react";
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider"; import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection"; import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
import useTimelineLoader from "../../hooks/use-timeline-loader"; import { PointerCommunityCard } from "./components/community-card";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useSubject from "../../hooks/use-subject";
import CommunityCard from "./components/community-card";
import { getEventUID } from "../../helpers/nostr/events";
import VerticalPageLayout from "../../components/vertical-page-layout"; import VerticalPageLayout from "../../components/vertical-page-layout";
import RelaySelectionButton from "../../components/relay-selection/relay-selection-button"; import { COMMUNITY_DEFINITION_KIND, SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER } from "../../helpers/nostr/communities";
import RelaySelectionProvider, { useRelaySelectionContext } from "../../providers/relay-selection-provider";
import { COMMUNITY_DEFINITION_KIND, validateCommunity } from "../../helpers/nostr/communities";
import { NostrEvent } from "../../types/nostr-event";
import { NostrQuery } from "../../types/nostr-query";
import { ErrorBoundary } from "../../components/error-boundary"; import { ErrorBoundary } from "../../components/error-boundary";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useSubjects from "../../hooks/use-subjects";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import { NOTE_LIST_KIND, getCoordinatesFromList } from "../../helpers/nostr/lists";
import { useNavigate } from "react-router-dom";
import { ChevronLeftIcon } from "../../components/icons";
import { parseCoordinate } from "../../helpers/nostr/events";
import UserAvatarLink from "../../components/user-avatar-link";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
export function useUsersJoinedCommunitiesLists(pubkeys: string[], additionalRelays: string[] = []) {
const readRelays = useReadRelayUrls(additionalRelays);
const muteListSubjects = useMemo(() => {
return pubkeys.map((pubkey) =>
replaceableEventLoaderService.requestEvent(
readRelays,
NOTE_LIST_KIND,
pubkey,
SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER,
),
);
}, [pubkeys]);
return useSubjects(muteListSubjects);
}
function CommunityCardWithMembers({ pointer, pubkeys }: { pointer: AddressPointer; pubkeys: string[] }) {
return (
<ErrorBoundary>
<Flex direction="column" gap="2">
<AvatarGroup size="md">
{pubkeys.map((pubkey) => (
<UserAvatarLink key={pubkey} pubkey={pubkey} />
))}
</AvatarGroup>
<PointerCommunityCard pointer={pointer} maxW="xl" />
</Flex>
</ErrorBoundary>
);
}
function CommunitiesExplorePage() { function CommunitiesExplorePage() {
const { filter, listId } = usePeopleListContext(); const navigate = useNavigate();
const { relays } = useRelaySelectionContext(); const { people } = usePeopleListContext();
const showMore = useDisclosure();
const eventFilter = useCallback((event: NostrEvent) => { const communitiesLists = useUsersJoinedCommunitiesLists(people?.map((p) => p.pubkey) ?? []);
return validateCommunity(event);
}, []);
const query = useMemo(() => { const communityPointers = useMemo(() => {
const base: NostrQuery = { kinds: [COMMUNITY_DEFINITION_KIND] }; const dir = new Map<string, { pointer: AddressPointer; pubkeys: string[] }>();
if (filter?.authors) { for (const list of communitiesLists) {
base.authors = filter.authors; for (const { coordinate } of getCoordinatesFromList(list)) {
base["#p"] = filter.authors; const pointer = parseCoordinate(coordinate, true);
if (!pointer) continue;
if (pointer.kind === COMMUNITY_DEFINITION_KIND) {
if (dir.has(coordinate)) {
dir.get(coordinate)?.pubkeys.push(list.pubkey);
} else dir.set(coordinate, { pointer, pubkeys: [list.pubkey] });
} }
return base; }
}, [filter]); }
return dir;
}, [communitiesLists]);
const timeline = useTimelineLoader(`${listId}-browse-communities`, relays, query, { enabled: !!filter, eventFilter }); const sorted = Array.from(communityPointers.values()).sort((a, b) => b.pubkeys.length - a.pubkeys.length);
const communities = useSubject(timeline.timeline);
const callback = useTimelineCurserIntersectionCallback(timeline);
return ( return (
<IntersectionObserverProvider callback={callback}>
<VerticalPageLayout> <VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap"> <Flex gap="2" alignItems="center" wrap="wrap">
<PeopleListSelection /> <Button onClick={() => navigate(-1)} leftIcon={<ChevronLeftIcon />}>
<RelaySelectionButton /> Back
</Button>
<PeopleListSelection hideGlobalOption />
<Switch onChange={showMore.onToggle} checked={showMore.isOpen}>
Show More
</Switch>
</Flex> </Flex>
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}> <Flex gap="4" direction="column">
{communities.map((event) => ( {sorted
<ErrorBoundary> .filter((c) => (showMore ? c.pubkeys.length > 1 : true))
<CommunityCard key={getEventUID(event)} community={event} /> .map(({ pointer, pubkeys }) => (
</ErrorBoundary> <CommunityCardWithMembers
key={pointer.kind + pointer.pubkey + pointer.identifier}
pointer={pointer}
pubkeys={pubkeys}
/>
))} ))}
</SimpleGrid> </Flex>
</VerticalPageLayout> </VerticalPageLayout>
</IntersectionObserverProvider>
); );
} }
export default function ExploreCommunitiesView() { export default function ExploreCommunitiesView() {
return ( return (
<PeopleListProvider initList="global"> <PeopleListProvider>
<RelaySelectionProvider>
<CommunitiesExplorePage /> <CommunitiesExplorePage />
</RelaySelectionProvider>
</PeopleListProvider> </PeopleListProvider>
); );
} }

View File

@@ -1,22 +1,12 @@
import { Button, Center, Flex, Heading, Link, SimpleGrid, Text } from "@chakra-ui/react"; import { Button, Center, Flex, Heading, Link, SimpleGrid, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { nip19 } from "nostr-tools";
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 useSubscribedCommunitiesList from "../../hooks/use-subscribed-communities-list"; import useSubscribedCommunitiesList from "../../hooks/use-subscribed-communities-list";
import { useCurrentAccount } from "../../hooks/use-current-account"; import { useCurrentAccount } from "../../hooks/use-current-account";
import { EmbedEventPointer } from "../../components/embed-event"; import { PointerCommunityCard } from "./components/community-card";
import { AddressPointer } from "nostr-tools/lib/types/nip19";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import CommunityCard from "./components/community-card";
function LoadCommunityCard({ pointer }: { pointer: AddressPointer }) {
const community = useReplaceableEvent(pointer);
if (!community) return <span>Loading {pointer.identifier}</span>;
return <CommunityCard community={community} />;
}
function CommunitiesHomePage() { function CommunitiesHomePage() {
const account = useCurrentAccount()!; const account = useCurrentAccount()!;
@@ -33,7 +23,7 @@ function CommunitiesHomePage() {
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}> <SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
{communities.map((pointer) => ( {communities.map((pointer) => (
<ErrorBoundary key={pointer.kind + pointer.pubkey + pointer.identifier}> <ErrorBoundary key={pointer.kind + pointer.pubkey + pointer.identifier}>
<LoadCommunityCard pointer={pointer} /> <PointerCommunityCard pointer={pointer} />
</ErrorBoundary> </ErrorBoundary>
))} ))}
</SimpleGrid> </SimpleGrid>