Skip to content

Commit

Permalink
add community pending view
Browse files Browse the repository at this point in the history
  • Loading branch information
hzrd149 committed Oct 11, 2023
1 parent 0f66704 commit a582167
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 68 deletions.
11 changes: 10 additions & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 /> },
],
},
],
},
{
Expand Down
6 changes: 3 additions & 3 deletions src/classes/event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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++;
}
Expand All @@ -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++;
}
Expand Down
7 changes: 4 additions & 3 deletions src/classes/timeline-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
}
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/nostr/communities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

Expand Down
4 changes: 2 additions & 2 deletions src/hooks/use-timeline-loader.ts
Original file line number Diff line number Diff line change
@@ -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;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
);
}
103 changes: 47 additions & 56 deletions src/views/community/community-home.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,43 @@
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);
const mods = getCommunityMods(community);
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">
Expand All @@ -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">
Expand All @@ -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}>
Expand All @@ -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} />
Expand Down
69 changes: 69 additions & 0 deletions src/views/community/views/new.tsx
Original file line number Diff line number Diff line change
@@ -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} />
</>
);
}
Loading

0 comments on commit a582167

Please sign in to comment.