mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-19 12:00:32 +02:00
add community trending view
This commit is contained in:
@@ -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 /> },
|
||||
],
|
||||
},
|
||||
|
@@ -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 };
|
||||
}
|
||||
|
41
src/hooks/use-events-reactions.ts
Normal file
41
src/hooks/use-events-reactions.ts
Normal 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;
|
||||
}
|
@@ -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>
|
||||
|
@@ -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);
|
||||
|
76
src/views/community/views/trending.tsx
Normal file
76
src/views/community/views/trending.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user