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>
143 lines
4.3 KiB
TypeScript
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',
|
|
};
|
|
}
|