diff --git a/src/app.tsx b/src/app.tsx
index 9d6bc791d..cbd912a61 100644
--- a/src/app.tsx
+++ b/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: },
- { path: ":pubkey", element: },
+ {
+ path: ":pubkey",
+ element: ,
+ children: [
+ { path: "", element: },
+ { path: "pending", element: },
+ ],
+ },
],
},
{
diff --git a/src/classes/event-store.ts b/src/classes/event-store.ts
index 2af3dc6ac..24c186af0 100644
--- a/src/classes/event-store.ts
+++ b/src/classes/event-store.ts
@@ -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++;
}
diff --git a/src/classes/timeline-loader.ts b/src/classes/timeline-loader.ts
index e9e55f29d..03aad70b9 100644
--- a/src/classes/timeline-loader.ts
+++ b/src/classes/timeline-loader.ts
@@ -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();
}
diff --git a/src/helpers/nostr/communities.ts b/src/helpers/nostr/communities.ts
index 087091ed0..4b2e1bc31 100644
--- a/src/helpers/nostr/communities.ts
+++ b/src/helpers/nostr/communities.ts
@@ -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[];
}
diff --git a/src/hooks/use-timeline-loader.ts b/src/hooks/use-timeline-loader.ts
index 44c0c8eb2..53866015b 100644
--- a/src/hooks/use-timeline-loader.ts
+++ b/src/hooks/use-timeline-loader.ts
@@ -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;
};
diff --git a/src/views/communities/components/community-subscribe-button.tsx b/src/views/communities/components/community-subscribe-button.tsx
index 484229df7..a8bc967a3 100644
--- a/src/views/communities/components/community-subscribe-button.tsx
+++ b/src/views/communities/components/community-subscribe-button.tsx
@@ -50,8 +50,13 @@ export default function CommunityJoinButton({
}, [isSubscribed, list, community]);
return (
-
+ }
+ as={RouterLink}
+ to={getCommunityPath(community)}
+ colorScheme={active === "new" ? "primary" : "gray"}
+ >
+ New
+
+ }
+ as={RouterLink}
+ to={getCommunityPath(community) + "/pending"}
+ colorScheme={active == "pending" ? "primary" : "gray"}
+ >
+ Pending
+
+
+
+
diff --git a/src/views/community/views/new.tsx b/src/views/community/views/new.tsx
new file mode 100644
index 000000000..5257820ca
--- /dev/null
+++ b/src/views/community/views/new.tsx
@@ -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(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 (
+
+
+
+ );
+}
+
+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 (
+ <>
+
+ {approvals.map((approval) => (
+
+ ))}
+
+
+ >
+ );
+}
diff --git a/src/views/community/views/pending.tsx b/src/views/community/views/pending.tsx
new file mode 100644
index 000000000..ce414f52d
--- /dev/null
+++ b/src/views/community/views/pending.tsx
@@ -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(null);
+ useRegisterIntersectionEntity(ref, getEventUID(event));
+
+ return (
+
+
+
+ );
+}
+
+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();
+ 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 (
+ <>
+
+ {pending.map((event) => (
+
+ ))}
+
+
+ >
+ );
+}