From a582167d4d6d48e0d0c6ce31b31804a8d8c85f93 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Wed, 11 Oct 2023 09:14:46 -0500 Subject: [PATCH] add community pending view --- src/app.tsx | 11 +- src/classes/event-store.ts | 6 +- src/classes/timeline-loader.ts | 7 +- src/helpers/nostr/communities.ts | 2 +- src/hooks/use-timeline-loader.ts | 4 +- .../components/community-subscribe-button.tsx | 9 +- src/views/community/community-home.tsx | 103 ++++++++---------- src/views/community/views/new.tsx | 69 ++++++++++++ src/views/community/views/pending.tsx | 69 ++++++++++++ 9 files changed, 212 insertions(+), 68 deletions(-) create mode 100644 src/views/community/views/new.tsx create mode 100644 src/views/community/views/pending.tsx 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 ( - ); } diff --git a/src/views/community/community-home.tsx b/src/views/community/community-home.tsx index 4f1f4cd85..73116b389 100644 --- a/src/views/community/community-home.tsx +++ b/src/views/community/community-home.tsx @@ -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(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 ( - - - - ); -} +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 ( - + {description && ( <> - Description: + + Description: + )} - + Moderators: @@ -75,7 +50,7 @@ function CommunityDetails({ community }: { community: NostrEvent }) { {communityRelays.length > 0 && ( <> - + Relays: @@ -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 ( @@ -124,13 +97,31 @@ export default function CommunityHomePage({ community }: { community: NostrEvent - - - - {approvals.map((approval) => ( - - ))} - + + + + + + + + + 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) => ( + + ))} + + + + ); +}