124 lines
4.6 KiB
TypeScript
124 lines
4.6 KiB
TypeScript
/**
|
|
* Batch community confidence for many spots (map grid / lists). One relay query
|
|
* for all coords, then a distinct-account, tier-weighted level per spot.
|
|
*
|
|
* ⚠️ Unlike the single-spot `useSpotCommunityScore`, the batch does NOT apply
|
|
* web-of-trust / activity weighting — resolving that for hundreds of actors
|
|
* would be its own fan-out. It still counts distinct accounts, drops the
|
|
* mapper's own signals, and caps each account's contribution.
|
|
*/
|
|
import { useMemo } from 'react';
|
|
import { useQuery } from '@tanstack/react-query';
|
|
import type { NostrEvent } from '@nostrify/nostrify';
|
|
|
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
|
import { SKATE_SPOT_KIND, SKATE_CREW_KIND, LAB_KINDS } from '@/lib/types';
|
|
import {
|
|
WEIGHT,
|
|
PER_ACCOUNT_CAP,
|
|
emptyBreakdown,
|
|
classifySignal,
|
|
communityLevel,
|
|
type AccountSignals,
|
|
type SpotCommunityBreakdown,
|
|
type SpotCommunityLevel,
|
|
} from '@/hooks/useSpotCommunityScore';
|
|
|
|
export interface SpotCommunityMini {
|
|
level: SpotCommunityLevel;
|
|
breakdown: SpotCommunityBreakdown;
|
|
}
|
|
|
|
const blankSignals = (): AccountSignals => ({
|
|
photos: 0, statusReports: 0, comments: 0, reactions: 0, zaps: 0, zapSats: 0, collections: 0, tagSuggestions: 0, crews: 0, labRefs: 0,
|
|
});
|
|
|
|
export function useBatchSpotCommunityScores(
|
|
spots: Array<{ id: string; pubkey: string; identifier: string }>,
|
|
) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
|
|
const refs = useMemo(
|
|
() => spots.map((s) => `${SKATE_SPOT_KIND}:${s.pubkey}:${s.identifier}`),
|
|
[spots],
|
|
);
|
|
const mapperByRef = useMemo(() => {
|
|
const m = new Map<string, string>();
|
|
for (const s of spots) m.set(`${SKATE_SPOT_KIND}:${s.pubkey}:${s.identifier}`, s.pubkey);
|
|
return m;
|
|
}, [spots]);
|
|
|
|
return useQuery<Map<string, SpotCommunityMini>>({
|
|
queryKey: ['batch-spot-community', refs.slice().sort().join(','), getQueryKeySuffix()],
|
|
enabled: refs.length > 0,
|
|
staleTime: 2 * 60 * 1000,
|
|
queryFn: async (c) => {
|
|
const out = new Map<string, SpotCommunityMini>();
|
|
if (refs.length === 0) return out;
|
|
|
|
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]);
|
|
const events = await querySkateContent(
|
|
[
|
|
{ kinds: [1063], '#a': refs, limit: 400 },
|
|
{ kinds: [2259], '#a': refs, limit: 400 },
|
|
{ kinds: [1111], '#A': refs, limit: 400 },
|
|
{ kinds: [7], '#a': refs, limit: 400 },
|
|
{ kinds: [9735], '#a': refs, limit: 400 },
|
|
{ kinds: [30003], '#a': refs, limit: 400 },
|
|
{ kinds: [SKATE_CREW_KIND], '#a': refs, limit: 300 },
|
|
{ kinds: [...LAB_KINDS], '#a': refs, limit: 400 },
|
|
],
|
|
{ signal },
|
|
).catch(() => [] as NostrEvent[]);
|
|
|
|
// coord → (account → signals)
|
|
const perCoord = new Map<string, Map<string, AccountSignals>>();
|
|
const seen = new Set<string>();
|
|
for (const ev of events) {
|
|
if (seen.has(ev.id)) continue;
|
|
seen.add(ev.id);
|
|
const ref = ev.tags.find(([n, v]) => n === 'a' && v?.startsWith(`${SKATE_SPOT_KIND}:`))?.[1];
|
|
if (!ref || !mapperByRef.has(ref)) continue;
|
|
if (ev.pubkey === mapperByRef.get(ref)) continue; // the mapper's own signals never count
|
|
const key = classifySignal(ev);
|
|
if (!key) continue;
|
|
let accounts = perCoord.get(ref);
|
|
if (!accounts) { accounts = new Map(); perCoord.set(ref, accounts); }
|
|
const acc = accounts.get(ev.pubkey) ?? blankSignals();
|
|
acc[key] += 1;
|
|
accounts.set(ev.pubkey, acc);
|
|
}
|
|
|
|
for (const [ref, accounts] of perCoord) {
|
|
const b = emptyBreakdown();
|
|
let score = 0;
|
|
for (const a of accounts.values()) {
|
|
b.photos += a.photos;
|
|
b.statusReports += a.statusReports;
|
|
b.comments += a.comments;
|
|
b.reactions += a.reactions;
|
|
b.zaps += a.zaps;
|
|
b.zapSats += a.zapSats;
|
|
b.collections += a.collections;
|
|
b.tagSuggestions += a.tagSuggestions;
|
|
b.crews += a.crews;
|
|
b.labRefs += a.labRefs;
|
|
const raw =
|
|
a.photos * WEIGHT.photo +
|
|
a.statusReports * WEIGHT.status +
|
|
a.zaps * WEIGHT.zap +
|
|
a.comments * WEIGHT.comment +
|
|
a.reactions * WEIGHT.reaction +
|
|
a.collections * WEIGHT.collection +
|
|
a.tagSuggestions * WEIGHT.tagSuggestion +
|
|
a.crews * WEIGHT.crew +
|
|
a.labRefs * WEIGHT.labRef;
|
|
score += Math.min(raw, PER_ACCOUNT_CAP);
|
|
}
|
|
b.skaters = accounts.size;
|
|
out.set(ref, { level: communityLevel(b, score), breakdown: b });
|
|
}
|
|
return out;
|
|
},
|
|
});
|
|
}
|