Second pass of TRUST_SCORE.md. - useTrustAssertions now batches: every TrustBadge registers its subject into a shared per-viewer union and ONE query fetches the union (no per-card query), with a debounced refetch when a list grows. Same hook API. - A direct follow (or self) is no longer shown as Verified - only a friend-of-friend or a provider assertion counts; Flagged is never suppressed. - useUserReports reads public NIP-56 reports for an account, and TrustFlaggedNotice shows a flagged counterparty with the reporters grouped by the viewer's network plus Report / Dismiss. Wired into the listing seller card and the coach detail page. - New walletUi.trust.* keys (en/es).
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
|
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
|
import { onlyVerified } from '@/lib/verifyEvents';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
|
|
/** NIP-56 report kind. */
|
|
export const REPORT_KIND = 1984;
|
|
|
|
export interface UserReport {
|
|
id: string;
|
|
/** The reporter. */
|
|
pubkey: string;
|
|
createdAt: number;
|
|
content: string;
|
|
/** NIP-56 reason from the `p` tag, when present. */
|
|
reason?: string;
|
|
}
|
|
|
|
/**
|
|
* Public user-level reports (NIP-56) that name this pubkey in a `p` tag, read
|
|
* from the community relay + the viewer's relays. This is the auditable side of
|
|
* a "flagged" account: the provider asserts a count, but the raw reports are
|
|
* public, so we can show who said it in aggregate.
|
|
*/
|
|
export function useUserReports(pubkey: string | undefined) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
|
|
const query = useQuery({
|
|
queryKey: ['user-reports', pubkey, getQueryKeySuffix()],
|
|
enabled: !!pubkey,
|
|
staleTime: 60_000,
|
|
queryFn: async (c): Promise<UserReport[]> => {
|
|
if (!pubkey) return [];
|
|
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(8000)]);
|
|
const events = onlyVerified(await querySkateContent(
|
|
[{ kinds: [REPORT_KIND], '#p': [pubkey], limit: 200 }],
|
|
{ signal },
|
|
));
|
|
return events.map(toUserReport).filter((r): r is UserReport => r !== null);
|
|
},
|
|
});
|
|
|
|
const reporters = useMemo(() => {
|
|
const set = new Set<string>();
|
|
for (const report of query.data ?? []) set.add(report.pubkey);
|
|
return [...set];
|
|
}, [query.data]);
|
|
|
|
return {
|
|
reports: query.data ?? [],
|
|
reporterPubkeys: reporters,
|
|
/** Distinct reporters — the anti-brigade unit, same as content reports. */
|
|
reporterCount: reporters.length,
|
|
isLoading: query.isLoading,
|
|
};
|
|
}
|
|
|
|
function toUserReport(event: NostrEvent): UserReport | null {
|
|
const pTag = event.tags.find(([name]) => name === 'p');
|
|
if (!pTag) return null;
|
|
return {
|
|
id: event.id,
|
|
pubkey: event.pubkey,
|
|
createdAt: event.created_at,
|
|
content: event.content,
|
|
reason: pTag[2],
|
|
};
|
|
}
|