mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-11 13:20:37 +02:00
show latest posts on communities home page
This commit is contained in:
parent
1b8a1d299c
commit
8a03bbb60e
@ -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) => (
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
17
src/views/community/components/community-approved-post.tsx
Normal file
17
src/views/community/components/community-approved-post.tsx
Normal 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;
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 (
|
||||
|
@ -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} />
|
||||
|
@ -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"
|
||||
|
@ -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} />
|
||||
|
Loading…
x
Reference in New Issue
Block a user