mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-22 15:19:47 +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 CommunityView from "./views/community/index";
|
||||||
import CommunityPendingView from "./views/community/views/pending";
|
import CommunityPendingView from "./views/community/views/pending";
|
||||||
import CommunityNewestView from "./views/community/views/newest";
|
import CommunityNewestView from "./views/community/views/newest";
|
||||||
|
import CommunityTrendingView from "./views/community/views/trending";
|
||||||
|
|
||||||
import RelaysView from "./views/relays";
|
import RelaysView from "./views/relays";
|
||||||
import RelayView from "./views/relays/relay";
|
import RelayView from "./views/relays/relay";
|
||||||
@@ -253,6 +254,8 @@ const router = createHashRouter([
|
|||||||
element: <CommunityView />,
|
element: <CommunityView />,
|
||||||
children: [
|
children: [
|
||||||
{ path: "", element: <CommunityNewestView /> },
|
{ path: "", element: <CommunityNewestView /> },
|
||||||
|
{ path: "trending", element: <CommunityTrendingView /> },
|
||||||
|
{ path: "newest", element: <CommunityNewestView /> },
|
||||||
{ path: "pending", element: <CommunityPendingView /> },
|
{ path: "pending", element: <CommunityPendingView /> },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { validateEvent } from "nostr-tools";
|
import { validateEvent } from "nostr-tools";
|
||||||
import { NostrEvent, isDTag, isETag, isPTag } from "../../types/nostr-event";
|
import { NostrEvent, isDTag, isETag, isPTag } from "../../types/nostr-event";
|
||||||
import { getMatchLink, getMatchNostrLink } from "../regexp";
|
import { getMatchLink, getMatchNostrLink } from "../regexp";
|
||||||
|
import { ReactionGroup } from "./reactions";
|
||||||
|
|
||||||
export const SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER = "communities";
|
export const SUBSCRIBED_COMMUNITIES_LIST_IDENTIFIER = "communities";
|
||||||
export const COMMUNITY_DEFINITION_KIND = 34550;
|
export const COMMUNITY_DEFINITION_KIND = 34550;
|
||||||
@@ -81,3 +82,10 @@ export function buildApprovalMap(events: Iterable<NostrEvent>, mods: string[]) {
|
|||||||
}
|
}
|
||||||
return approvals;
|
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],
|
"#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("/pending")) active = "pending";
|
||||||
|
if (location.pathname.endsWith("/trending")) active = "trending";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -97,15 +99,21 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
|||||||
New Post
|
New Post
|
||||||
</Button>
|
</Button>
|
||||||
<Divider orientation="vertical" h="2rem" />
|
<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
|
Trending
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<Clock />}
|
leftIcon={<Clock />}
|
||||||
as={RouterLink}
|
as={RouterLink}
|
||||||
to={getCommunityPath(community)}
|
to={getCommunityPath(community) + "/newest"}
|
||||||
|
colorScheme={active === "newest" ? "primary" : "gray"}
|
||||||
replace
|
replace
|
||||||
colorScheme={active === "new" ? "primary" : "gray"}
|
|
||||||
>
|
>
|
||||||
New
|
New
|
||||||
</Button>
|
</Button>
|
||||||
@@ -113,8 +121,8 @@ export default function CommunityHomePage({ community }: { community: NostrEvent
|
|||||||
leftIcon={<Hourglass03 />}
|
leftIcon={<Hourglass03 />}
|
||||||
as={RouterLink}
|
as={RouterLink}
|
||||||
to={getCommunityPath(community) + "/pending"}
|
to={getCommunityPath(community) + "/pending"}
|
||||||
replace
|
|
||||||
colorScheme={active == "pending" ? "primary" : "gray"}
|
colorScheme={active == "pending" ? "primary" : "gray"}
|
||||||
|
replace
|
||||||
>
|
>
|
||||||
Pending
|
Pending
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -6,7 +6,7 @@ import useEventReactions from "../../../hooks/use-event-reactions";
|
|||||||
import { useSigningContext } from "../../../providers/signing-provider";
|
import { useSigningContext } from "../../../providers/signing-provider";
|
||||||
import { draftEventReaction, groupReactions } from "../../../helpers/nostr/reactions";
|
import { draftEventReaction, groupReactions } from "../../../helpers/nostr/reactions";
|
||||||
import clientRelaysService from "../../../services/client-relays";
|
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 { unique } from "../../../helpers/array";
|
||||||
import eventReactionsService from "../../../services/event-reactions";
|
import eventReactionsService from "../../../services/event-reactions";
|
||||||
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
import NostrPublishAction from "../../../classes/nostr-publish-action";
|
||||||
@@ -23,9 +23,7 @@ export default function PostVoteButtons({
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const grouped = useMemo(() => groupReactions(reactions ?? []), [reactions]);
|
const grouped = useMemo(() => groupReactions(reactions ?? []), [reactions]);
|
||||||
const up = grouped.find((r) => r.emoji === "+");
|
const { vote, up, down } = getCommunityPostVote(grouped);
|
||||||
const down = grouped.find((r) => r.emoji === "-");
|
|
||||||
const vote = (up?.pubkeys.length ?? 0) - (down?.pubkeys.length ?? 0);
|
|
||||||
|
|
||||||
const hasUpVote = !!account && !!up?.pubkeys.includes(account.pubkey);
|
const hasUpVote = !!account && !!up?.pubkeys.includes(account.pubkey);
|
||||||
const hasDownVote = !!account && !!down?.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