add community trending view

This commit is contained in:
hzrd149
2023-10-25 10:33:37 -05:00
parent c6a8a48f00
commit b150aac326
6 changed files with 143 additions and 9 deletions

View File

@@ -63,6 +63,7 @@ import CommunityFindByNameView from "./views/community/find-by-name";
import CommunityView from "./views/community/index";
import CommunityPendingView from "./views/community/views/pending";
import CommunityNewestView from "./views/community/views/newest";
import CommunityTrendingView from "./views/community/views/trending";
import RelaysView from "./views/relays";
import RelayView from "./views/relays/relay";
@@ -253,6 +254,8 @@ const router = createHashRouter([
element: <CommunityView />,
children: [
{ path: "", element: <CommunityNewestView /> },
{ path: "trending", element: <CommunityTrendingView /> },
{ path: "newest", element: <CommunityNewestView /> },
{ path: "pending", element: <CommunityPendingView /> },
],
},

View File

@@ -1,6 +1,7 @@
import { validateEvent } from "nostr-tools";
import { NostrEvent, isDTag, isETag, isPTag } from "../../types/nostr-event";
import { getMatchLink, getMatchNostrLink } from "../regexp";
import { ReactionGroup } from "./reactions";
export const SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER = "communities";
export const COMMUNITY_DEFINITION_KIND = 34550;
@@ -81,3 +82,10 @@ export function buildApprovalMap(events: Iterable<NostrEvent>, mods: string[]) {
}
return approvals;
}
export function getCommunityPostVote(grouped: ReactionGroup[]) {
const up = grouped.find((r) => r.emoji === "+");
const down = grouped.find((r) => r.emoji === "-");
const vote = (up?.pubkeys.length ?? 0) - (down?.pubkeys.length ?? 0);
return { up, down, vote };
}

View File

@@ -0,0 +1,41 @@
import { useEffect, useMemo, useState } from "react";
import eventReactionsService from "../services/event-reactions";
import { useReadRelayUrls } from "./use-client-relays";
import { NostrEvent } from "../types/nostr-event";
import Subject from "../classes/subject";
export default function useEventsReactions(eventIds: string[], additionalRelays: string[] = [], alwaysRequest = true) {
const relays = useReadRelayUrls(additionalRelays);
// get subjects
const subjects = useMemo(() => {
const dir: Record<string, Subject<NostrEvent[]>> = {};
for (const eventId of eventIds) {
dir[eventId] = eventReactionsService.requestReactions(eventId, relays, alwaysRequest);
}
return dir;
}, [eventIds, relays.join("|"), alwaysRequest]);
// get values out of subjects
const reactions: Record<string, NostrEvent[]> = {};
for (const [id, subject] of Object.entries(subjects)) {
if (subject.value) reactions[id] = subject.value;
}
const [_, update] = useState(0);
// subscribe to subjects
useEffect(() => {
const listener = () => update((v) => v + 1);
for (const [_, sub] of Object.entries(subjects)) {
sub?.subscribe(listener, undefined, false);
}
return () => {
for (const [_, sub] of Object.entries(subjects)) {
sub?.unsubscribe(listener, undefined);
}
};
}, [subjects, update]);
return reactions;
}

View File

@@ -48,8 +48,10 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
"#a": [communityCoordinate],
});
let active = "new";
let active = "newest";
if (location.pathname.endsWith("/newest")) active = "newest";
if (location.pathname.endsWith("/pending")) active = "pending";
if (location.pathname.endsWith("/trending")) active = "trending";
return (
<>
@@ -97,15 +99,21 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
New Post
</Button>
<Divider orientation="vertical" h="2rem" />
<Button leftIcon={<TrendUp01 />} isDisabled>
<Button
leftIcon={<TrendUp01 />}
as={RouterLink}
to={getCommunityPath(community) + "/trending"}
colorScheme={active === "trending" ? "primary" : "gray"}
replace
>
Trending
</Button>
<Button
leftIcon={<Clock />}
as={RouterLink}
to={getCommunityPath(community)}
to={getCommunityPath(community) + "/newest"}
colorScheme={active === "newest" ? "primary" : "gray"}
replace
colorScheme={active === "new" ? "primary" : "gray"}
>
New
</Button>
@@ -113,8 +121,8 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
leftIcon={<Hourglass03 />}
as={RouterLink}
to={getCommunityPath(community) + "/pending"}
replace
colorScheme={active == "pending" ? "primary" : "gray"}
replace
>
Pending
</Button>

View File

@@ -6,7 +6,7 @@ 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 { getCommunityRelays } from "../../../helpers/nostr/communities";
import { getCommunityPostVote, getCommunityRelays } from "../../../helpers/nostr/communities";
import { unique } from "../../../helpers/array";
import eventReactionsService from "../../../services/event-reactions";
import NostrPublishAction from "../../../classes/nostr-publish-action";
@@ -23,9 +23,7 @@ export default function PostVoteButtons({
const toast = useToast();
const grouped = useMemo(() => groupReactions(reactions ?? []), [reactions]);
const up = grouped.find((r) => r.emoji === "+");
const down = grouped.find((r) => r.emoji === "-");
const vote = (up?.pubkeys.length ?? 0) - (down?.pubkeys.length ?? 0);
const { vote, up, down } = getCommunityPostVote(grouped);
const hasUpVote = !!account && !!up?.pubkeys.includes(account.pubkey);
const hasDownVote = !!account && !!down?.pubkeys.includes(account.pubkey);

View File

@@ -0,0 +1,76 @@
import { memo, useMemo } from "react";
import { Flex } from "@chakra-ui/react";
import { useOutletContext } from "react-router-dom";
import {
buildApprovalMap,
getCommunityMods,
getCommunityPostVote,
getCommunityRelays,
} from "../../../helpers/nostr/communities";
import useSubject from "../../../hooks/use-subject";
import { useTimelineCurserIntersectionCallback } from "../../../hooks/use-timeline-cursor-intersection-callback";
import { NostrEvent } from "../../../types/nostr-event";
import IntersectionObserverProvider from "../../../providers/intersection-observer";
import TimelineActionAndStatus from "../../../components/timeline-page/timeline-action-and-status";
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";
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>
);
},
);
export default function CommunityTrendingView() {
const { community, timeline } = useOutletContext() as { community: NostrEvent; timeline: TimelineLoader };
const muteFilter = useUserMuteFilter();
const mods = getCommunityMods(community);
const events = useSubject(timeline.timeline);
const approvalMap = buildApprovalMap(events, mods);
const approved = events
.filter((e) => approvalMap.has(e.id))
.map((event) => ({ event, approvals: approvalMap.get(event.id) }))
.filter((e) => !muteFilter(e.event));
// fetch votes for approved posts
const eventReactions = useEventsReactions(
approved.map((e) => e.event.id),
getCommunityRelays(community),
);
const eventVotes = useMemo(() => {
const dir: Record<string, number> = {};
for (const [id, reactions] of Object.entries(eventReactions)) {
const grouped = groupReactions(reactions);
const { vote } = getCommunityPostVote(grouped);
dir[id] = vote;
}
return dir;
}, [eventReactions]);
const sorted = approved.sort((a, b) => (eventVotes[b.event.id] ?? 0) - (eventVotes[a.event.id] ?? 0));
const callback = useTimelineCurserIntersectionCallback(timeline);
return (
<>
<IntersectionObserverProvider callback={callback}>
{sorted.map(({ event, approvals }) => (
<ApprovedEvent key={event.id} event={event} approvals={approvals ?? []} community={community} />
))}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
</>
);
}