SkateSpots/src/hooks/useTrustScore.ts
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

143 lines
4.3 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { useNostr } from '@nostrify/react';
import { useCurrentUser } from './useCurrentUser';
/**
* Hook to calculate trust score for a Nostr pubkey using Web of Trust (WoT).
*
* Trust scores are calculated using social graph analysis:
* - 100: Self (viewing your own content)
* - 90: Direct follow (you follow them)
* - 60: Friend-of-friend (you follow someone who follows them)
* - 30: Has NIP-05 verification
* - 0: No connection or verification
*
* @param targetPubkey - The pubkey to calculate trust score for
* @returns Trust score (0-100) or undefined if not available
*/
export function useTrustScore(targetPubkey: string | undefined) {
const { nostr } = useNostr();
const { user } = useCurrentUser();
return useQuery({
queryKey: ['trust-score', targetPubkey, user?.pubkey],
queryFn: async (c) => {
if (!targetPubkey) return undefined;
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(5000)]);
try {
// Self score = 100
if (user?.pubkey && targetPubkey === user.pubkey) {
return 100;
}
// 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) {
console.warn('[useTrustScore] No follow list found for source pubkey');
return 0;
}
// Extract followed pubkeys from tags
const following = new Set(
followList.tags
.filter(([tag]) => tag === 'p')
.map(([, pubkey]) => pubkey)
);
// Direct follow = 90
if (following.has(targetPubkey)) {
console.log(`[useTrustScore] ${targetPubkey.slice(0, 8)} is directly followed: 90`);
return 90;
}
// Check friend-of-friend (2 hops)
const friendsFollowLists = await nostr.query(
[{ kinds: [3], authors: Array.from(following), limit: 100 }],
{ signal }
);
for (const friendList of friendsFollowLists) {
const friendFollowing = friendList.tags
.filter(([tag]) => tag === 'p')
.map(([, pubkey]) => pubkey);
if (friendFollowing.includes(targetPubkey)) {
console.log(`[useTrustScore] ${targetPubkey.slice(0, 8)} is friend-of-friend: 60`);
return 60;
}
}
// Check if they have NIP-05 verification (basic credibility)
const profileEvents = await nostr.query(
[{ kinds: [0], authors: [targetPubkey], limit: 1 }],
{ signal }
);
if (profileEvents[0]) {
try {
const metadata = JSON.parse(profileEvents[0].content);
if (metadata.nip05) {
console.log(`[useTrustScore] ${targetPubkey.slice(0, 8)} has NIP-05: 30`);
return 30;
}
} catch (e) {
// Ignore JSON parse errors
}
}
// No trust signals found
console.log(`[useTrustScore] ${targetPubkey.slice(0, 8)} has no trust signals: 0`);
return 0;
} catch (error) {
console.warn('[useTrustScore] Error calculating trust score:', error);
return undefined;
}
},
enabled: !!targetPubkey,
staleTime: 3600000, // Cache for 1 hour (trust scores change slowly)
gcTime: 86400000, // Keep in cache for 24 hours
retry: 1, // Only retry once on failure
});
}
/**
* Get trust badge configuration based on trust score
*/
export function getTrustBadgeConfig(score: number | undefined) {
if (score === undefined || score < 50) {
return {
show: false,
variant: 'outline' as const,
className: 'border-gray-500 text-gray-500',
label: 'UNVERIFIED',
icon: null,
};
}
if (score < 80) {
return {
show: true,
variant: 'outline' as const,
className: 'border-yellow-500 text-yellow-500',
label: 'TRUSTED',
icon: 'shield',
};
}
return {
show: true,
variant: 'default' as const,
className: 'bg-green-500 text-white',
label: 'VERIFIED',
icon: 'check',
};
}