SkateSpots/src/hooks/useBatchTrustScores.ts
shakespeare.diy adc90caae9 #2 Silence expected 'no follow list' warn in useBatchTrustScores
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-08-27 10:19:23 +07:00

122 lines
4 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { useNostr } from '@nostrify/react';
import { useCurrentUser } from './useCurrentUser';
/**
* Hook to fetch trust scores for multiple pubkeys in batch.
* More efficient than calling useTrustScore multiple times.
*
* @param pubkeys - Array of pubkeys to calculate trust scores for
* @returns Map of pubkey to trust score (0-100)
*/
export function useBatchTrustScores(pubkeys: string[]) {
const { nostr } = useNostr();
const { user } = useCurrentUser();
return useQuery({
queryKey: ['batch-trust-scores', pubkeys.sort().join(','), user?.pubkey],
queryFn: async (c) => {
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(8000)]);
const scores = new Map<string, number>();
try {
// Get user's follow list (kind 3)
const sourcePubkey = user?.pubkey || '20e7c953439e66f3882ab96fe0faa8ba246d6fb12c03b0cb4e2dc4623b8bcb21';
const followListEvents = await nostr.query(
[{ kinds: [3], authors: [sourcePubkey], limit: 1 }],
{ signal }
);
const followList = followListEvents[0];
if (!followList) {
// No follow list (e.g. logged-out or new account) — default all to 0.
pubkeys.forEach((pk) => scores.set(pk, 0));
return scores;
}
// Extract followed pubkeys
const following = new Set(
followList.tags
.filter(([tag]) => tag === 'p')
.map(([, pubkey]) => pubkey)
);
// Process each pubkey
for (const targetPubkey of pubkeys) {
// Self score = 100
if (user?.pubkey && targetPubkey === user.pubkey) {
scores.set(targetPubkey, 100);
continue;
}
// Direct follow = 90
if (following.has(targetPubkey)) {
scores.set(targetPubkey, 90);
continue;
}
// Default to 0 (will check friend-of-friend and NIP-05 later)
scores.set(targetPubkey, 0);
}
// Get friend-of-friend relationships (batch query)
const uncheckedPubkeys = pubkeys.filter((pk) => scores.get(pk) === 0);
if (uncheckedPubkeys.length > 0) {
const friendsFollowLists = await nostr.query(
[{ kinds: [3], authors: Array.from(following), limit: 100 }],
{ signal }
);
// Build a set of all friends-of-friends
const friendsOfFriends = new Set<string>();
for (const friendList of friendsFollowLists) {
friendList.tags
.filter(([tag]) => tag === 'p')
.forEach(([, pubkey]) => friendsOfFriends.add(pubkey));
}
// Check friend-of-friend = 60
for (const pubkey of uncheckedPubkeys) {
if (friendsOfFriends.has(pubkey)) {
scores.set(pubkey, 60);
}
}
}
// Check NIP-05 verification for remaining 0-score pubkeys
const unverifiedPubkeys = pubkeys.filter((pk) => scores.get(pk) === 0);
if (unverifiedPubkeys.length > 0) {
const profileEvents = await nostr.query(
[{ kinds: [0], authors: unverifiedPubkeys }],
{ signal }
);
for (const profileEvent of profileEvents) {
try {
const metadata = JSON.parse(profileEvent.content);
if (metadata.nip05) {
scores.set(profileEvent.pubkey, 30);
}
} catch (e) {
// Ignore JSON parse errors
}
}
}
return scores;
} catch (error) {
console.warn('[useBatchTrustScores] Error calculating batch trust scores:', error);
// Return default scores of 0
pubkeys.forEach((pk) => scores.set(pk, 0));
return scores;
}
},
enabled: pubkeys.length > 0,
staleTime: 3600000, // Cache for 1 hour
gcTime: 86400000, // Keep in cache for 24 hours
retry: 1,
});
}