SkateSpots/src/hooks/useSellerInfo.ts
SkateSpots 21a0390ed5 refactor(relays): route every relay consumer through useRelays()
So Local relays are used and Blocked relays are excluded EVERYWHERE, not just in
the NPool routers:
- useListingReports, useSellerInfo, useSkateRelays (read+write), useZaps
  (NIP-57 relays tag), usePurchasePro, CollectionDetailPage, CrewForm,
  useDirectMessages write fallback
- AppProvider's empty-relays defaults check is the only intentional direct reader
- AGENTS.md updated

Gate: 101 tests, 0 errors/warnings, boot smoke green.
2026-09-26 22:36:14 +07:00

156 lines
4.9 KiB
TypeScript

import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';
import type { NostrEvent } from '@nostrify/nostrify';
import { MARKETPLACE_ITEM_KIND } from '@/lib/types';
import { useRelays } from '@/lib/relays';
export interface SellerInfo {
pubkey: string;
accountAge: number; // Days since first event
listingCount: number;
activeListingCount: number;
hasNip05: boolean;
nip05?: string;
firstSeen?: number; // Unix timestamp of first event
}
/**
* Hook to fetch seller information including account age and listing count.
*/
export function useSellerInfo(pubkey: string | undefined) {
const { nostr } = useNostr();
const { read } = useRelays();
const relayUrls = read.slice().sort().join(',');
return useQuery({
queryKey: ['seller-info', pubkey, relayUrls],
queryFn: async (c) => {
if (!pubkey) return null;
const signal = AbortSignal.any([c.signal, AbortSignal.timeout(10000)]);
try {
const readRelays = read;
// Query profile (kind 0) and marketplace listings in parallel
const profileEventsPromise: Promise<NostrEvent[]>[] = [];
const listingEventsPromise: Promise<NostrEvent[]>[] = [];
for (const relayUrl of readRelays) {
try {
const relay = nostr.relay(relayUrl);
profileEventsPromise.push(
relay.query(
[{ kinds: [0], authors: [pubkey], limit: 1 }],
{ signal }
).catch(() => [])
);
listingEventsPromise.push(
relay.query(
[{
kinds: [MARKETPLACE_ITEM_KIND],
authors: [pubkey],
'#t': ['skateboarding'],
limit: 100
}],
{ signal }
).catch(() => [])
);
} catch {
// Ignore individual relay errors
}
}
const [profileResults, listingResults] = await Promise.all([
Promise.all(profileEventsPromise),
Promise.all(listingEventsPromise),
]);
// Flatten results
const profileEvents = profileResults.flat();
const listingEvents = listingResults.flat();
// Get profile metadata
let hasNip05 = false;
let nip05: string | undefined;
let profileCreatedAt: number | undefined;
if (profileEvents.length > 0) {
const profile = profileEvents.reduce((latest, current) =>
current.created_at > latest.created_at ? current : latest
);
try {
const metadata = JSON.parse(profile.content);
hasNip05 = !!metadata.nip05;
nip05 = metadata.nip05;
} catch {
// Ignore JSON parse errors
}
profileCreatedAt = profile.created_at;
}
// Deduplicate listings by d-tag
const listingMap = new Map<string, NostrEvent>();
for (const event of listingEvents) {
const d = event.tags.find(([name]: [string]) => name === 'd')?.[1];
if (d) {
const key = `${event.pubkey}:${event.kind}:${d}`;
const existing = listingMap.get(key);
if (!existing || event.created_at > existing.created_at) {
listingMap.set(key, event);
}
}
}
const uniqueListings = Array.from(listingMap.values());
// Count active listings (not expired, status = active)
const now = Math.floor(Date.now() / 1000);
const activeListings = uniqueListings.filter(event => {
const status = event.tags.find(([name]: [string]) => name === 'status')?.[1];
const expiration = event.tags.find(([name]: [string]) => name === 'expiration')?.[1];
if (status && status !== 'active') return false;
if (expiration && parseInt(expiration) < now) return false;
return true;
});
// Find oldest event timestamp (approximation of account age)
const allTimestamps = [
...uniqueListings.map(e => e.created_at),
...(profileCreatedAt ? [profileCreatedAt] : []),
];
const firstSeen = allTimestamps.length > 0
? Math.min(...allTimestamps)
: undefined;
const accountAge = firstSeen
? Math.floor((now - firstSeen) / (24 * 60 * 60)) // Days
: 0;
return {
pubkey,
accountAge,
listingCount: uniqueListings.length,
activeListingCount: activeListings.length,
hasNip05,
nip05,
firstSeen,
} as SellerInfo;
} catch (error) {
console.warn('[useSellerInfo] Error fetching seller info:', error);
return null;
}
},
enabled: !!pubkey,
staleTime: 300000, // Cache for 5 minutes
gcTime: 600000, // Keep in cache for 10 minutes
});
}