show latest posts on communities home page

This commit is contained in:
hzrd149 2023-10-26 15:51:27 -05:00
parent 1b8a1d299c
commit 8a03bbb60e
10 changed files with 172 additions and 83 deletions

View File

@ -2,20 +2,23 @@ import { Modal, ModalOverlay, ModalContent, ModalBody, ModalCloseButton, Flex }
import { ModalProps } from "@chakra-ui/react";
import { nip19 } from "nostr-tools";
import { NostrEvent } from "../../types/nostr-event";
import { NostrEvent, isATag } from "../../types/nostr-event";
import RawJson from "./raw-json";
import RawValue from "./raw-value";
import RawPre from "./raw-pre";
import userMetadataService from "../../services/user-metadata";
import { getUserDisplayName } from "../../helpers/user-metadata";
import { getEventCoordinate } from "../../helpers/nostr/events";
import { COMMUNITY_DEFINITION_KIND } from "../../helpers/nostr/communities";
export default function CommunityPostDebugModal({
event,
community,
approvals,
...props
}: { event: NostrEvent; community: NostrEvent; approvals: NostrEvent[] } & Omit<ModalProps, "children">) {
}: { event: NostrEvent; approvals: NostrEvent[] } & Omit<ModalProps, "children">) {
const communityCoordinate = event.tags
.filter(isATag)
.find((t) => t[1].startsWith(COMMUNITY_DEFINITION_KIND + ":"))?.[1];
return (
<Modal {...props}>
<ModalOverlay />
@ -25,7 +28,7 @@ export default function CommunityPostDebugModal({
<Flex gap="2" direction="column">
<RawValue heading="Event Id" value={event.id} />
<RawValue heading="Encoded id (NIP-19)" value={nip19.noteEncode(event.id)} />
<RawValue heading="Community Coordinate" value={getEventCoordinate(community)} />
<RawValue heading="Community Coordinate" value={communityCoordinate} />
<RawPre heading="Content" value={event.content} />
<RawJson heading="JSON" json={event} />
{approvals.map((approval) => (

View File

@ -1,5 +1,5 @@
import { useMemo } from "react";
import { AvatarGroup, Button, Flex, Switch, useDisclosure } from "@chakra-ui/react";
import { AvatarGroup, Button, Flex, SimpleGrid, Switch, useDisclosure } from "@chakra-ui/react";
import PeopleListProvider, { usePeopleListContext } from "../../providers/people-list-provider";
import PeopleListSelection from "../../components/people-list-selection/people-list-selection";
@ -83,7 +83,7 @@ function CommunitiesExplorePage() {
Show More
</Switch>
</Flex>
<Flex gap="4" direction="column">
<SimpleGrid spacing="4" columns={{ base: 1, lg: 2 }}>
{sorted
.filter((c) => (showMore ? c.pubkeys.length > 1 : true))
.map(({ pointer, pubkeys }) => (
@ -93,7 +93,7 @@ function CommunitiesExplorePage() {
pubkeys={pubkeys}
/>
))}
</Flex>
</SimpleGrid>
</VerticalPageLayout>
);
}

View File

@ -1,4 +1,24 @@
import { Button, Center, Flex, Heading, Link, SimpleGrid, Text, useDisclosure, useToast } from "@chakra-ui/react";
import { Kind } from "nostr-tools";
import { useMemo } from "react";
import {
Button,
ButtonGroup,
Center,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerHeader,
DrawerOverlay,
Flex,
Heading,
Link,
SimpleGrid,
Switch,
Text,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { Link as RouterLink, useNavigate } from "react-router-dom";
import { Navigate } from "react-router-dom";
import dayjs from "dayjs";
@ -7,16 +27,32 @@ import VerticalPageLayout from "../../components/vertical-page-layout";
import { ErrorBoundary } from "../../components/error-boundary";
import useSubscribedCommunitiesList from "../../hooks/use-subscribed-communities-list";
import { useCurrentAccount } from "../../hooks/use-current-account";
import { PointerCommunityCard } from "./components/community-card";
import CommunityCard from "./components/community-card";
import CommunityCreateModal, { FormValues } from "./components/community-create-modal";
import { useSigningContext } from "../../providers/signing-provider";
import { DraftNostrEvent } from "../../types/nostr-event";
import { COMMUNITY_DEFINITION_KIND, getCommunityName } from "../../helpers/nostr/communities";
import {
COMMUNITY_APPROVAL_KIND,
COMMUNITY_DEFINITION_KIND,
buildApprovalMap,
getCommunityMods,
getCommunityName,
} from "../../helpers/nostr/communities";
import NostrPublishAction from "../../classes/nostr-publish-action";
import { unique } from "../../helpers/array";
import clientRelaysService from "../../services/client-relays";
import replaceableEventLoaderService from "../../services/replaceable-event-requester";
import replaceableEventLoaderService, { createCoordinate } from "../../services/replaceable-event-requester";
import { getImageSize } from "../../helpers/image";
import { useReadRelayUrls } from "../../hooks/use-client-relays";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useSubject from "../../hooks/use-subject";
import useUserMuteFilter from "../../hooks/use-user-mute-filter";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import useReplaceableEvents from "../../hooks/use-replaceable-events";
import { getEventCoordinate } from "../../helpers/nostr/events";
import IntersectionObserverProvider from "../../providers/intersection-observer";
import ApprovedEvent from "../community/components/community-approved-post";
import TimelineActionAndStatus from "../../components/timeline-page/timeline-action-and-status";
function CommunitiesHomePage() {
const toast = useToast();
@ -24,7 +60,12 @@ function CommunitiesHomePage() {
const navigate = useNavigate();
const account = useCurrentAccount()!;
const createModal = useDisclosure();
const { pointers: communities } = useSubscribedCommunitiesList(account.pubkey, { alwaysRequest: true });
const readRelays = useReadRelayUrls();
const { pointers: communityCoordinates } = useSubscribedCommunitiesList(account.pubkey, { alwaysRequest: true });
const communities = useReplaceableEvents(communityCoordinates, readRelays).sort(
(a, b) => b.created_at - a.created_at,
);
const createCommunity = async (values: FormValues) => {
try {
@ -64,26 +105,81 @@ function CommunitiesHomePage() {
}
};
const timeline = useTimelineLoader(
`all-communities-timeline`,
readRelays,
{
kinds: [Kind.Text, Kind.Repost, COMMUNITY_APPROVAL_KIND],
"#a": communityCoordinates.map((p) => createCoordinate(p.kind, p.pubkey, p.identifier)),
},
{ enabled: communityCoordinates.length > 0 },
);
const showUnapproved = useDisclosure();
const muteFilter = useUserMuteFilter();
const mods = useMemo(() => {
const set = new Set<string>();
for (const community of communities) {
for (const pubkey of getCommunityMods(community)) {
set.add(pubkey);
}
}
return Array.from(set);
}, [communities]);
const events = useSubject(timeline.timeline);
const approvalMap = buildApprovalMap(events, mods);
const approved = events
.filter((event) => event.kind !== COMMUNITY_APPROVAL_KIND)
.filter((e) => (showUnapproved.isOpen ? true : approvalMap.has(e.id)))
.map((event) => ({ event, approvals: approvalMap.get(event.id) }))
.filter((e) => !muteFilter(e.event));
const callback = useTimelineCurserIntersectionCallback(timeline);
const communityDrawer = useDisclosure();
return (
<>
<VerticalPageLayout>
<Flex gap="2" alignItems="center" wrap="wrap">
<Button as={RouterLink} to="/communities/explore">
Explore Communities
Explore
</Button>
<Button ml="auto" onClick={createModal.onOpen}>
Create Community
</Button>
<ButtonGroup ml="auto">
<Button onClick={createModal.onOpen}>Create</Button>
<Button onClick={communityDrawer.onOpen} hideFrom="xl">
Joined
</Button>
</ButtonGroup>
</Flex>
{communities.length > 0 ? (
<SimpleGrid spacing="2" columns={{ base: 1, lg: 2 }}>
{communities.map((pointer) => (
<ErrorBoundary key={pointer.kind + pointer.pubkey + pointer.identifier}>
<PointerCommunityCard pointer={pointer} />
</ErrorBoundary>
))}
</SimpleGrid>
<Flex gap="4" overflow="hidden">
<Flex direction="column" gap="2" flex={1} overflow="hidden">
<Flex alignItems="center" gap="4">
<Heading size="lg">Latest Posts</Heading>
<Switch isChecked={showUnapproved.isOpen} onChange={showUnapproved.onToggle}>
Show Unapproved
</Switch>
</Flex>
<IntersectionObserverProvider callback={callback}>
{approved.map(({ event, approvals }) => (
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} />
))}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
</Flex>
<Flex gap="2" direction="column" w="md" flexShrink={0} hideBelow="xl">
<Heading size="md">Joined Communities</Heading>
{communities.map((community) => (
<ErrorBoundary key={getEventCoordinate(community)}>
<CommunityCard community={community} />
</ErrorBoundary>
))}
</Flex>
</Flex>
) : (
<Center aspectRatio={3 / 4} flexDirection="column" gap="4">
<Heading size="md">No communities :(</Heading>
@ -99,6 +195,22 @@ function CommunitiesHomePage() {
{createModal.isOpen && (
<CommunityCreateModal isOpen={createModal.isOpen} onClose={createModal.onClose} onSubmit={createCommunity} />
)}
<Drawer isOpen={communityDrawer.isOpen} placement="right" onClose={communityDrawer.onClose} size="lg">
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader p="4">Joined Communities</DrawerHeader>
<DrawerBody display="flex" flexDirection="column" gap="2" px="4" py="0">
{communities.map((community) => (
<ErrorBoundary key={getEventCoordinate(community)}>
<CommunityCard community={community} />
</ErrorBoundary>
))}
</DrawerBody>
</DrawerContent>
</Drawer>
</>
);
}

View File

@ -0,0 +1,17 @@
import { memo } from "react";
import { Flex } from "@chakra-ui/react";
import PostVoteButtons from "./post-vote-buttions";
import CommunityPost from "./community-post";
import { NostrEvent } from "../../../types/nostr-event";
const ApprovedEvent = memo(({ event, approvals }: { event: NostrEvent; approvals: NostrEvent[] }) => {
return (
<Flex gap="2" alignItems="flex-start">
<PostVoteButtons event={event} flexShrink={0} />
<CommunityPost event={event} approvals={approvals} flex={1} />
</Flex>
);
});
export default ApprovedEvent;

View File

@ -22,10 +22,9 @@ import CommunityPostDebugModal from "../../../components/debug-modals/community-
export default function CommunityPostMenu({
event,
community,
approvals,
...props
}: Omit<MenuIconButtonProps, "children"> & { event: NostrEvent; community: NostrEvent; approvals: NostrEvent[] }) {
}: Omit<MenuIconButtonProps, "children"> & { event: NostrEvent; approvals: NostrEvent[] }) {
const account = useCurrentAccount();
const debugModal = useDisclosure();
@ -79,7 +78,6 @@ export default function CommunityPostMenu({
onClose={debugModal.onClose}
size="6xl"
approvals={approvals}
community={community}
/>
)}
</>

View File

@ -40,7 +40,6 @@ export function ApprovalIcon({ approval }: { approval: NostrEvent }) {
export type CommunityPostPropTypes = {
event: NostrEvent;
approvals: NostrEvent[];
community: NostrEvent;
};
function PostSubject({ event }: { event: NostrEvent }) {
@ -84,7 +83,6 @@ function Approvals({ approvals }: { approvals: NostrEvent[] }) {
export function CommunityTextPost({
event,
approvals,
community,
...props
}: Omit<CardProps, "children"> & CommunityPostPropTypes) {
const ref = useRef<HTMLDivElement | null>(null);
@ -102,14 +100,7 @@ export function CommunityTextPost({
</Text>
<Flex gap="2" alignItems="center" ml="auto">
{approvals.length > 0 && <Approvals approvals={approvals} />}
<CommunityPostMenu
event={event}
community={community}
approvals={approvals}
aria-label="More Options"
size="xs"
variant="ghost"
/>
<CommunityPostMenu event={event} approvals={approvals} aria-label="More Options" size="xs" variant="ghost" />
</Flex>
</CardFooter>
</Card>
@ -119,7 +110,6 @@ export function CommunityTextPost({
export function CommunityRepostPost({
event,
approvals,
community,
...props
}: Omit<CardProps, "children"> & CommunityPostPropTypes) {
const encodedRepost = parseHardcodedNoteContent(event);
@ -152,14 +142,7 @@ export function CommunityRepostPost({
</Text>
<Flex gap="2" alignItems="center" ml="auto">
{approvals.length > 0 && <Approvals approvals={approvals} />}
<CommunityPostMenu
event={event}
community={community}
approvals={approvals}
aria-label="More Options"
size="xs"
variant="ghost"
/>
<CommunityPostMenu event={event} approvals={approvals} aria-label="More Options" size="xs" variant="ghost" />
</Flex>
</CardFooter>
</Card>
@ -169,14 +152,13 @@ export function CommunityRepostPost({
export default function CommunityPost({
event,
approvals,
community,
...props
}: Omit<CardProps, "children"> & CommunityPostPropTypes) {
switch (event.kind) {
case Kind.Text:
return <CommunityTextPost event={event} approvals={approvals} community={community} {...props} />;
return <CommunityTextPost event={event} approvals={approvals} {...props} />;
case Kind.Repost:
return <CommunityRepostPost event={event} approvals={approvals} community={community} {...props} />;
return <CommunityRepostPost event={event} approvals={approvals} {...props} />;
}
return null;
}

View File

@ -6,21 +6,19 @@ import useEventReactions from "../../../hooks/use-event-reactions";
import { useSigningContext } from "../../../providers/signing-provider";
import { draftEventReaction, groupReactions } from "../../../helpers/nostr/reactions";
import clientRelaysService from "../../../services/client-relays";
import { getCommunityPostVote, getCommunityRelays } from "../../../helpers/nostr/communities";
import { getCommunityPostVote } from "../../../helpers/nostr/communities";
import { unique } from "../../../helpers/array";
import eventReactionsService from "../../../services/event-reactions";
import NostrPublishAction from "../../../classes/nostr-publish-action";
import { ChevronDownIcon, ChevronUpIcon } from "../../../components/icons";
import { NostrEvent } from "../../../types/nostr-event";
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
export default function PostVoteButtons({
event,
community,
...props
}: Omit<CardProps, "children"> & { event: NostrEvent; community: NostrEvent }) {
export default function PostVoteButtons({ event, ...props }: Omit<CardProps, "children"> & { event: NostrEvent }) {
const account = useCurrentAccount();
const reactions = useEventReactions(event.id);
const toast = useToast();
const additionalRelays = useAdditionalRelayContext();
const grouped = useMemo(() => groupReactions(reactions ?? []), [reactions]);
const { vote, up, down } = getCommunityPostVote(grouped);
@ -39,8 +37,7 @@ export default function PostVoteButtons({
const signed = await requestSignature(draft);
if (signed) {
const writeRelays = clientRelaysService.getWriteUrls();
const communityRelays = getCommunityRelays(community);
new NostrPublishAction("Reaction", unique([...writeRelays, ...communityRelays]), signed);
new NostrPublishAction("Reaction", unique([...writeRelays, ...additionalRelays]), signed);
eventReactionsService.handleEvent(signed);
}
} catch (e) {
@ -48,7 +45,7 @@ export default function PostVoteButtons({
}
setLoading(false);
},
[event, community, requestSignature],
[event, requestSignature, additionalRelays],
);
return (

View File

@ -12,17 +12,7 @@ import PostVoteButtons from "../components/post-vote-buttions";
import TimelineLoader from "../../../classes/timeline-loader";
import CommunityPost from "../components/community-post";
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
const ApprovedEvent = memo(
({ event, approvals, community }: { event: NostrEvent; approvals: NostrEvent[]; community: NostrEvent }) => {
return (
<Flex gap="2" alignItems="flex-start">
<PostVoteButtons event={event} community={community} flexShrink={0} />
<CommunityPost event={event} community={community} approvals={approvals} flex={1} />
</Flex>
);
},
);
import ApprovedEvent from "../components/community-approved-post";
export default function CommunityNewestView() {
const { community, timeline } = useOutletContext() as { community: NostrEvent; timeline: TimelineLoader };
@ -43,7 +33,7 @@ export default function CommunityNewestView() {
<>
<IntersectionObserverProvider callback={callback}>
{approved.map(({ event, approvals }) => (
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} community={community} />
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} />
))}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />

View File

@ -65,7 +65,7 @@ function ModPendingPost({ event, community, approvals }: PendingProps) {
return (
<Flex direction="column" gap="2" ref={ref}>
<CommunityPost event={event} approvals={approvals} community={community} />
<CommunityPost event={event} approvals={approvals} />
<Flex gap="2">
<Button
colorScheme="primary"

View File

@ -19,17 +19,7 @@ import CommunityPost from "../components/community-post";
import useUserMuteFilter from "../../../hooks/use-user-mute-filter";
import useEventsReactions from "../../../hooks/use-events-reactions";
import { groupReactions } from "../../../helpers/nostr/reactions";
const ApprovedEvent = memo(
({ event, approvals, community }: { event: NostrEvent; approvals: NostrEvent[]; community: NostrEvent }) => {
return (
<Flex gap="2" alignItems="flex-start">
<PostVoteButtons event={event} community={community} flexShrink={0} />
<CommunityPost event={event} community={community} approvals={approvals} flex={1} />
</Flex>
);
},
);
import ApprovedEvent from "../components/community-approved-post";
export default function CommunityTrendingView() {
const { community, timeline } = useOutletContext() as { community: NostrEvent; timeline: TimelineLoader };
@ -67,7 +57,7 @@ export default function CommunityTrendingView() {
<>
<IntersectionObserverProvider callback={callback}>
{sorted.map(({ event, approvals }) => (
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} community={community} />
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} />
))}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />