mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 21:31:43 +01:00
add community pending view
This commit is contained in:
parent
0f667048b6
commit
a582167d4d
11
src/app.tsx
11
src/app.tsx
@ -59,6 +59,8 @@ import BadgeDetailsView from "./views/badges/badge-details";
|
||||
import CommunitiesHomeView from "./views/communities";
|
||||
import CommunityFindByNameView from "./views/community/find-by-name";
|
||||
import CommunityView from "./views/community/index";
|
||||
import CommunityPendingView from "./views/community/views/pending";
|
||||
import CommunityNewView from "./views/community/views/new";
|
||||
|
||||
import RelaysView from "./views/relays";
|
||||
import RelayView from "./views/relays/relay";
|
||||
@ -216,7 +218,14 @@ const router = createHashRouter([
|
||||
path: "c/:community",
|
||||
children: [
|
||||
{ path: "", element: <CommunityFindByNameView /> },
|
||||
{ path: ":pubkey", element: <CommunityView /> },
|
||||
{
|
||||
path: ":pubkey",
|
||||
element: <CommunityView />,
|
||||
children: [
|
||||
{ path: "", element: <CommunityNewView /> },
|
||||
{ path: "pending", element: <CommunityPendingView /> },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -2,7 +2,7 @@ import { getEventUID } from "../helpers/nostr/events";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import Subject from "./subject";
|
||||
|
||||
export type EventFilter = (event: NostrEvent) => boolean;
|
||||
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
|
||||
|
||||
export default class EventStore {
|
||||
name?: string;
|
||||
@ -59,7 +59,7 @@ export default class EventStore {
|
||||
while (true) {
|
||||
const event = events.shift();
|
||||
if (!event) return;
|
||||
if (filter && !filter(event)) continue;
|
||||
if (filter && !filter(event, this)) continue;
|
||||
if (i === nth) return event;
|
||||
i++;
|
||||
}
|
||||
@ -71,7 +71,7 @@ export default class EventStore {
|
||||
while (true) {
|
||||
const event = events.pop();
|
||||
if (!event) return;
|
||||
if (filter && !filter(event)) continue;
|
||||
if (filter && !filter(event, this)) continue;
|
||||
if (i === nth) return event;
|
||||
i++;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ function addToQuery(filter: NostrRequestFilter, query: NostrQuery) {
|
||||
|
||||
const BLOCK_SIZE = 30;
|
||||
|
||||
export type EventFilter = (event: NostrEvent) => boolean;
|
||||
export type EventFilter = (event: NostrEvent, store: EventStore) => boolean;
|
||||
|
||||
export class RelayTimelineLoader {
|
||||
relay: string;
|
||||
@ -134,7 +134,8 @@ export default class TimelineLoader {
|
||||
|
||||
private updateTimeline() {
|
||||
if (this.eventFilter) {
|
||||
this.timeline.next(this.events.getSortedEvents().filter(this.eventFilter));
|
||||
const filter = this.eventFilter;
|
||||
this.timeline.next(this.events.getSortedEvents().filter((e) => filter(e, this.events)));
|
||||
} else this.timeline.next(this.events.getSortedEvents());
|
||||
}
|
||||
private handleEvent(event: NostrEvent) {
|
||||
@ -207,7 +208,7 @@ export default class TimelineLoader {
|
||||
// update the subscription with the new query
|
||||
this.subscription.setQuery(addToQuery(query, { limit: BLOCK_SIZE / 2 }));
|
||||
}
|
||||
setFilter(filter?: (event: NostrEvent) => boolean) {
|
||||
setFilter(filter?: EventFilter) {
|
||||
this.eventFilter = filter;
|
||||
this.updateTimeline();
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export function getCommunityMods(community: NostrEvent) {
|
||||
const mods = community.tags.filter((t) => isPTag(t) && t[1] && t[3] === "moderator").map((t) => t[1]) as string[];
|
||||
return mods;
|
||||
}
|
||||
export function getCOmmunityRelays(community: NostrEvent) {
|
||||
export function getCommunityRelays(community: NostrEvent) {
|
||||
return community.tags.filter((t) => t[0] === "relay" && t[1]).map((t) => t[1]) as string[];
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useUnmount } from "react-use";
|
||||
import { NostrRequestFilter } from "../types/nostr-query";
|
||||
import { NostrEvent } from "../types/nostr-event";
|
||||
import timelineCacheService from "../services/timeline-cache";
|
||||
import { EventFilter } from "../classes/timeline-loader";
|
||||
|
||||
type Options = {
|
||||
enabled?: boolean;
|
||||
eventFilter?: (event: NostrEvent) => boolean;
|
||||
eventFilter?: EventFilter;
|
||||
cursor?: number;
|
||||
};
|
||||
|
||||
|
@ -50,8 +50,13 @@ export default function CommunityJoinButton({
|
||||
}, [isSubscribed, list, community]);
|
||||
|
||||
return (
|
||||
<Button onClick={handleClick} {...props}>
|
||||
{isSubscribed ? "Unsubscribe" : "Subscribe"}
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
variant={isSubscribed ? "outline" : "solid"}
|
||||
colorScheme={isSubscribed ? "red" : "green"}
|
||||
{...props}
|
||||
>
|
||||
{isSubscribed ? "Leave" : "Join"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
@ -1,53 +1,26 @@
|
||||
import { useRef } from "react";
|
||||
import { Box, Button, Card, Flex, Heading, Text } from "@chakra-ui/react";
|
||||
import { Box, Button, ButtonGroup, Card, Flex, Heading, Text } from "@chakra-ui/react";
|
||||
import { Outlet, Link as RouterLink, useLocation } from "react-router-dom";
|
||||
import { nip19 } from "nostr-tools";
|
||||
|
||||
import {
|
||||
COMMUNITY_APPROVAL_KIND,
|
||||
getApprovedEmbeddedNote,
|
||||
getCOmmunityRelays as getCommunityRelays,
|
||||
getCommunityRelays as getCommunityRelays,
|
||||
getCommunityImage,
|
||||
getCommunityMods,
|
||||
getCommunityName,
|
||||
getCommunityDescription,
|
||||
} from "../../helpers/nostr/communities";
|
||||
import { NostrEvent, isETag } from "../../types/nostr-event";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import VerticalPageLayout from "../../components/vertical-page-layout";
|
||||
import { UserAvatarLink } from "../../components/user-avatar-link";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import CommunityDescription from "../communities/components/community-description";
|
||||
import useTimelineLoader from "../../hooks/use-timeline-loader";
|
||||
import { getEventCoordinate, getEventUID } from "../../helpers/nostr/events";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { unique } from "../../helpers/array";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
|
||||
import CommunityJoinButton from "../communities/components/community-subscribe-button";
|
||||
import useSingleEvent from "../../hooks/use-single-event";
|
||||
import { EmbedEvent } from "../../components/embed-event";
|
||||
import { AdditionalRelayProvider, useAdditionalRelayContext } from "../../providers/additional-relay-context";
|
||||
import { AdditionalRelayProvider } from "../../providers/additional-relay-context";
|
||||
import { RelayIconStack } from "../../components/relay-icon-stack";
|
||||
|
||||
function ApprovedEvent({ approval }: { approval: NostrEvent }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(approval));
|
||||
|
||||
const additionalRelays = useAdditionalRelayContext();
|
||||
const embeddedEvent = getApprovedEmbeddedNote(approval);
|
||||
const eventTag = approval.tags.find(isETag);
|
||||
|
||||
const loadEvent = useSingleEvent(
|
||||
eventTag?.[1],
|
||||
eventTag?.[2] ? [eventTag[2], ...additionalRelays] : additionalRelays,
|
||||
);
|
||||
const event = loadEvent || embeddedEvent;
|
||||
if (!event) return;
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<EmbedEvent event={event} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
import TrendUp01 from "../../components/icons/trend-up-01";
|
||||
import Clock from "../../components/icons/clock";
|
||||
import Hourglass03 from "../../components/icons/hourglass-03";
|
||||
|
||||
function CommunityDetails({ community }: { community: NostrEvent }) {
|
||||
const communityRelays = getCommunityRelays(community);
|
||||
@ -55,14 +28,16 @@ function CommunityDetails({ community }: { community: NostrEvent }) {
|
||||
const description = getCommunityDescription(community);
|
||||
|
||||
return (
|
||||
<Card p="2" w="xs" flexShrink={0}>
|
||||
<Card p="4" w="xs" flexShrink={0}>
|
||||
{description && (
|
||||
<>
|
||||
<Heading size="sm">Description:</Heading>
|
||||
<Heading size="sm" mb="2">
|
||||
Description:
|
||||
</Heading>
|
||||
<CommunityDescription community={community} maxLength={256} showExpand />
|
||||
</>
|
||||
)}
|
||||
<Heading size="sm" mt="2">
|
||||
<Heading size="sm" mt="4" mb="2">
|
||||
Moderators:
|
||||
</Heading>
|
||||
<Flex direction="column" gap="2">
|
||||
@ -75,7 +50,7 @@ function CommunityDetails({ community }: { community: NostrEvent }) {
|
||||
</Flex>
|
||||
{communityRelays.length > 0 && (
|
||||
<>
|
||||
<Heading size="sm" mt="2">
|
||||
<Heading size="sm" mt="4" mb="2">
|
||||
Relays:
|
||||
</Heading>
|
||||
<Flex direction="column" gap="2">
|
||||
@ -87,20 +62,18 @@ function CommunityDetails({ community }: { community: NostrEvent }) {
|
||||
);
|
||||
}
|
||||
|
||||
function getCommunityPath(community: NostrEvent) {
|
||||
return `/c/${encodeURIComponent(getCommunityName(community))}/${nip19.npubEncode(community.pubkey)}`;
|
||||
}
|
||||
|
||||
export default function CommunityHomePage({ community }: { community: NostrEvent }) {
|
||||
const mods = getCommunityMods(community);
|
||||
const image = getCommunityImage(community);
|
||||
const location = useLocation();
|
||||
|
||||
const communityRelays = getCommunityRelays(community);
|
||||
const readRelays = useReadRelayUrls(communityRelays);
|
||||
const timeline = useTimelineLoader(`${getEventUID(community)}-approved-posts`, readRelays, {
|
||||
authors: unique([community.pubkey, ...mods]),
|
||||
kinds: [COMMUNITY_APPROVAL_KIND],
|
||||
"#a": [getEventCoordinate(community)],
|
||||
});
|
||||
|
||||
const approvals = useSubject(timeline.timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
let active = "new";
|
||||
if (location.pathname.endsWith("/pending")) active = "pending";
|
||||
|
||||
return (
|
||||
<AdditionalRelayProvider relays={communityRelays}>
|
||||
@ -124,13 +97,31 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
||||
<CommunityJoinButton community={community} ml="auto" />
|
||||
</Flex>
|
||||
|
||||
<Flex gap="2" alignItems="flex-start">
|
||||
<Flex direction="column" gap="2" flex={1}>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{approvals.map((approval) => (
|
||||
<ApprovedEvent key={getEventUID(approval)} approval={approval} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
<Flex gap="4" alignItems="flex-start">
|
||||
<Flex direction="column" gap="4" flex={1}>
|
||||
<ButtonGroup size="sm">
|
||||
<Button leftIcon={<TrendUp01 />} isDisabled>
|
||||
Trending
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<Clock />}
|
||||
as={RouterLink}
|
||||
to={getCommunityPath(community)}
|
||||
colorScheme={active === "new" ? "primary" : "gray"}
|
||||
>
|
||||
New
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<Hourglass03 />}
|
||||
as={RouterLink}
|
||||
to={getCommunityPath(community) + "/pending"}
|
||||
colorScheme={active == "pending" ? "primary" : "gray"}
|
||||
>
|
||||
Pending
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Outlet context={{ community }} />
|
||||
</Flex>
|
||||
|
||||
<CommunityDetails community={community} />
|
||||
|
69
src/views/community/views/new.tsx
Normal file
69
src/views/community/views/new.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useRef } from "react";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
|
||||
import { unique } from "../../../helpers/array";
|
||||
import {
|
||||
COMMUNITY_APPROVAL_KIND,
|
||||
getApprovedEmbeddedNote,
|
||||
getCommunityMods,
|
||||
getCommunityRelays,
|
||||
} from "../../../helpers/nostr/communities";
|
||||
import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/events";
|
||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||
import { NostrEvent, isETag } from "../../../types/nostr-event";
|
||||
import { EmbedEvent } from "../../../components/embed-event";
|
||||
import useSingleEvent from "../../../hooks/use-single-event";
|
||||
import { useAdditionalRelayContext } from "../../../providers/additional-relay-context";
|
||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
|
||||
|
||||
function ApprovedEvent({ approval }: { approval: NostrEvent }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(approval));
|
||||
|
||||
const additionalRelays = useAdditionalRelayContext();
|
||||
const embeddedEvent = getApprovedEmbeddedNote(approval);
|
||||
const eventTag = approval.tags.find(isETag);
|
||||
|
||||
const loadEvent = useSingleEvent(
|
||||
eventTag?.[1],
|
||||
eventTag?.[2] ? [eventTag[2], ...additionalRelays] : additionalRelays,
|
||||
);
|
||||
const event = loadEvent || embeddedEvent;
|
||||
if (!event) return;
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<EmbedEvent event={event} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CommunityNewView() {
|
||||
const { community } = useOutletContext() as { community: NostrEvent };
|
||||
const mods = getCommunityMods(community);
|
||||
|
||||
const readRelays = useReadRelayUrls(getCommunityRelays(community));
|
||||
const timeline = useTimelineLoader(`${getEventUID(community)}-approved-posts`, readRelays, {
|
||||
authors: unique([community.pubkey, ...mods]),
|
||||
kinds: [COMMUNITY_APPROVAL_KIND],
|
||||
"#a": [getEventCoordinate(community)],
|
||||
});
|
||||
|
||||
const approvals = useSubject(timeline.timeline);
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{approvals.map((approval) => (
|
||||
<ApprovedEvent key={getEventUID(approval)} approval={approval} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</>
|
||||
);
|
||||
}
|
69
src/views/community/views/pending.tsx
Normal file
69
src/views/community/views/pending.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Box } from "@chakra-ui/react";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Kind } from "nostr-tools";
|
||||
|
||||
import { NostrEvent, isETag } from "../../../types/nostr-event";
|
||||
import { getEventCoordinate, getEventUID } from "../../../helpers/nostr/events";
|
||||
import { COMMUNITY_APPROVAL_KIND, getCommunityRelays } from "../../../helpers/nostr/communities";
|
||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||
import useTimelineLoader from "../../../hooks/use-timeline-loader";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import IntersectionObserverProvider, { useRegisterIntersectionEntity } from "../../../providers/intersection-observer";
|
||||
import { EmbedEvent } from "../../../components/embed-event";
|
||||
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
|
||||
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
|
||||
import EventStore from "../../../classes/event-store";
|
||||
|
||||
function PendingPost({ event }: { event: NostrEvent }) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
useRegisterIntersectionEntity(ref, getEventUID(event));
|
||||
|
||||
return (
|
||||
<Box ref={ref}>
|
||||
<EmbedEvent event={event} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CommunityPendingView() {
|
||||
const { community } = useOutletContext() as { community: NostrEvent };
|
||||
|
||||
const readRelays = useReadRelayUrls(getCommunityRelays(community));
|
||||
|
||||
const eventFilter = useCallback((event: NostrEvent, store: EventStore) => event.kind !== COMMUNITY_APPROVAL_KIND, []);
|
||||
const timeline = useTimelineLoader(
|
||||
`${getEventUID(community)}-pending-posts`,
|
||||
readRelays,
|
||||
{
|
||||
kinds: [Kind.Text, COMMUNITY_APPROVAL_KIND],
|
||||
"#a": [getEventCoordinate(community)],
|
||||
},
|
||||
{ eventFilter },
|
||||
);
|
||||
|
||||
const events = useSubject(timeline.timeline);
|
||||
|
||||
const approvals = new Set<string>();
|
||||
for (const [_, event] of timeline.events.events) {
|
||||
if (event.kind === COMMUNITY_APPROVAL_KIND) {
|
||||
for (const tag of event.tags) {
|
||||
if (isETag(tag)) approvals.add(tag[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const pending = events.filter((e) => !approvals.has(e.id));
|
||||
|
||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IntersectionObserverProvider callback={callback}>
|
||||
{pending.map((event) => (
|
||||
<PendingPost key={getEventUID(event)} event={event} />
|
||||
))}
|
||||
</IntersectionObserverProvider>
|
||||
<TimelineActionAndStatus timeline={timeline} />
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user