Files
eranos/src/hooks/useCommunityModeration.ts
T
2026-05-02 23:40:05 -07:00

73 lines
2.8 KiB
TypeScript

import { useMemo } from 'react';
import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
import type { NostrEvent } from '@nostrify/nostrify';
import { useCommunityMembers } from '@/hooks/useCommunityMembers';
import type { CommunityModerationContextValue } from '@/contexts/CommunityModerationContext';
import { COMMUNITY_DEFINITION_KIND, parseCommunityEvent } from '@/lib/communityUtils';
/**
* Resolve the community moderation context for a single event.
*
* Extracts the community tag from the event, fetches the community
* definition, resolves membership & moderation, and returns a value shaped
* for `CommunityModerationContext.Provider`. Callers install it as a
* Provider so that nested components (`NoteCard`, `NoteMoreMenu`) pick up
* the same moderation data via `useCommunityModerationContext()`.
*
* Returns `null` when:
* - The event has no community tag
* - The tag doesn't point to a kind 34550 community
* - The community definition hasn't loaded yet
*/
export function useCommunityModerationForEvent(event: NostrEvent): CommunityModerationContextValue | null {
const { nostr } = useNostr();
// Extract the community tag and its addressable parts in one pass.
// NIP-22 comments use uppercase A; NIP-75 goals link with lowercase a.
const parsed = useMemo(() => {
const aValue = event.tags.find(([n]) => n === 'A')?.[1]
?? event.tags.find(([n, v]) => n === 'a' && v?.startsWith(`${COMMUNITY_DEFINITION_KIND}:`))?.[1];
if (!aValue) return null;
if (!aValue.startsWith(`${COMMUNITY_DEFINITION_KIND}:`)) return null;
const parts = aValue.split(':');
if (parts.length < 3) return null;
return { aTag: aValue, pubkey: parts[1], dTag: parts.slice(2).join(':') };
}, [event.tags]);
// Fetch the community definition event.
const { data: communityEvent } = useQuery({
queryKey: ['community-definition', parsed?.aTag ?? ''],
queryFn: async ({ signal }) => {
if (!parsed) return null;
const events = await nostr.query(
[{
kinds: [COMMUNITY_DEFINITION_KIND],
authors: [parsed.pubkey],
'#d': [parsed.dTag],
limit: 1,
}],
{ signal: AbortSignal.any([signal, AbortSignal.timeout(8_000)]) },
);
return events[0] ?? null;
},
enabled: !!parsed,
staleTime: 5 * 60_000,
});
const community = useMemo(
() => (communityEvent ? parseCommunityEvent(communityEvent) : null),
[communityEvent],
);
// Reuses the same TanStack cache key as CommunityDetailPage, so opening a
// post detail page after the feed has already loaded is free.
const { moderation, rankMap } = useCommunityMembers(community);
return useMemo(() => {
if (!parsed) return null;
return { communityATag: parsed.aTag, moderation, rankMap };
}, [parsed, moderation, rankMap]);
}