SkateSpots/src/hooks/useNetworkAlerts.ts
SkateSpots 18a9e28f0b feat(trust): network alerts for reported accounts you follow
Fifth slice of TRUST_SCORE.md. useNetworkAlerts reads the viewer's NIP-85
assertions for the accounts they follow and surfaces those trusted people
reported or muted. New /alerts page lists them with Ignore / Unfollow /
Mute / Report (reusing useFollowActions, useMuteUser, ReportDialog) —
inform, never auto-act. Settings -> Trust links to it with a live count;
ignored accounts are device-local. /alerts added to the smoke list.
2026-10-09 18:32:47 +07:00

63 lines
2 KiB
TypeScript

import { useMemo } from 'react';
import { useFollowing } from './useFollowing';
import { useTrustAssertions } from './useTrustAssertions';
import { useLocalStorage } from './useLocalStorage';
import { FLAGGED_REPORTERS_THRESHOLD } from '@/lib/trustLadder';
/** A muted-by-trusted-people count at/above this also raises an alert. */
const MUTERS_THRESHOLD = FLAGGED_REPORTERS_THRESHOLD;
export interface NetworkAlert {
pubkey: string;
reporters: number;
muters: number;
followers?: number;
hops?: number;
}
/**
* Accounts the viewer follows that trusted people have reported or muted,
* according to the active NIP-85 provider.
*
* This is a safety notice, not an automatic action: the page offers
* ignore / unfollow / mute / report, and the user decides. Dismissed accounts
* are remembered device-locally.
*/
export function useNetworkAlerts() {
const { data: following } = useFollowing();
const { data: assertions, isLoading } = useTrustAssertions(following ?? []);
const [ignored, setIgnored] = useLocalStorage<string[]>('skatespots:trust:alerts-ignored', []);
const alerts = useMemo(() => {
const out: NetworkAlert[] = [];
for (const pubkey of following ?? []) {
if (ignored.includes(pubkey)) continue;
const assertion = assertions?.get(pubkey);
if (!assertion) continue;
const reporters = assertion.reporters ?? 0;
const muters = assertion.muters ?? 0;
if (reporters >= FLAGGED_REPORTERS_THRESHOLD || muters >= MUTERS_THRESHOLD) {
out.push({
pubkey,
reporters,
muters,
followers: assertion.followers,
hops: assertion.hops,
});
}
}
// Strongest negative signal first.
out.sort((a, b) => b.reporters + b.muters - (a.reporters + a.muters));
return out;
}, [following, assertions, ignored]);
return {
alerts,
alertCount: alerts.length,
isLoading,
ignore: (pubkey: string) =>
setIgnored((prev) => (prev.includes(pubkey) ? prev : [...prev, pubkey])),
};
}