import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { MODERATION_LABEL_NAMESPACE, MODERATION_LABEL_HIDDEN, } from '@/lib/types'; interface UseCrewModerationParams { /** Crew addressable coordinate `kind:pubkey:d`. */ crewCoord: string; /** Pubkeys allowed to moderate: the founder + delegated admins. */ moderators: string[]; enabled?: boolean; } /** * Fetch the set of crew-feed post IDs hidden by the crew's moderators * (founder + admins) via NIP-32 `hidden` labels. * * Only labels authored by a listed moderator are honored, so a random user * can't hide someone else's post. */ export function useCrewModeration({ crewCoord, moderators, enabled = true, }: UseCrewModerationParams) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); return useQuery({ queryKey: ['crew-moderation', crewCoord, moderators.slice().sort().join(','), getQueryKeySuffix()], queryFn: async (): Promise> => { if (moderators.length === 0) return new Set(); const signal = AbortSignal.timeout(8000); const events = await querySkateContent( [ { kinds: [1985], authors: moderators, '#a': [crewCoord], '#l': [MODERATION_LABEL_HIDDEN], limit: 200, }, ], { signal }, ); const modSet = new Set(moderators.map((m) => m.toLowerCase())); const hidden = new Set(); for (const ev of events) { // Validate namespace + label + moderator authorship. const hasNamespace = ev.tags.some( ([n, v]) => n === 'L' && v === MODERATION_LABEL_NAMESPACE, ); const hasLabel = ev.tags.some( ([n, v]) => n === 'l' && v === MODERATION_LABEL_HIDDEN, ); const refsCrew = ev.tags.some(([n, v]) => n === 'a' && v === crewCoord); if (!hasNamespace || !hasLabel || !refsCrew) continue; if (!modSet.has(ev.pubkey.toLowerCase())) continue; for (const [n, v] of ev.tags) { if (n === 'e' && v) hidden.add(v); } } return hidden; }, enabled: enabled && moderators.length > 0 && !!crewCoord, staleTime: 60 * 1000, }); } interface HidePostVars { postId: string; /** Author of the post being hidden (added as a `p` hint). */ postAuthor?: string; } /** * Publish a NIP-32 `hidden` moderation label for a crew-feed post. * The caller must be a crew moderator (gate on `useCrewRole().canManage`). */ export function useHideCrewPost(crewCoord: string) { const { mutateAsync: publish } = useNostrPublish(); const queryClient = useQueryClient(); return useMutation({ mutationFn: async ({ postId, postAuthor }: HidePostVars) => { const tags: string[][] = [ ['L', MODERATION_LABEL_NAMESPACE], ['l', MODERATION_LABEL_HIDDEN, MODERATION_LABEL_NAMESPACE], ['e', postId], ['a', crewCoord], ['alt', 'Crew moderation: post hidden'], ]; if (postAuthor) tags.push(['p', postAuthor]); await publish({ kind: 1985, content: '', tags }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['crew-moderation', crewCoord] }); }, }); }