import { useQuery } from '@tanstack/react-query'; import { useSkateRelays } from '@/hooks/useSkateRelays'; import { SKATE_CREW_KIND, CALENDAR_EVENT_KIND, type SkateCrew, } from '@/lib/types'; import { deriveActivity, type CrewActivity } from '@/lib/crewHelpers'; /** Build the addressable coordinate for a crew: `35006:pubkey:d`. */ export function crewCoordinate(crew: Pick): string { return `${SKATE_CREW_KIND}:${crew.pubkey}:${crew.identifier}`; } /** * Activity/health signals for a set of crews, in ONE batched query. * * We tally two signals per crew: * 1. **Feed freshness** — the most recent kind-1 note authored by the founder or * a listed member (proxy for "the crew is skating & posting"). * 2. **Upcoming events** — future calendar events (kind 31923) that carry an * `a` tag pointing back at the crew coordinate (crew-hosted jams/comps). * * Returns a map keyed by crew coordinate → {@link CrewActivity}. */ export function useCrewsActivity(crews: SkateCrew[] | undefined) { const { querySkateContent, getQueryKeySuffix } = useSkateRelays(); // Stable key from the crew set (coords only). const coords = (crews ?? []).map(crewCoordinate).sort(); const authorKey = (crews ?? []) .flatMap((c) => [c.pubkey, ...c.members]) .sort() .join(',') .slice(0, 512); return useQuery({ queryKey: ['crews-activity', coords.join(','), authorKey, getQueryKeySuffix()], enabled: !!crews && crews.length > 0, staleTime: 5 * 60 * 1000, gcTime: 30 * 60 * 1000, queryFn: async () => { const signal = AbortSignal.timeout(10000); const list = crews ?? []; // All feed authors (founder + members), de-duplicated. const authors = Array.from( new Set(list.flatMap((c) => [c.pubkey, ...c.members])), ); // Batched query: recent notes from any crew member + crew-hosted events. const [notes, events] = await Promise.all([ authors.length > 0 ? querySkateContent( [{ kinds: [1], authors, limit: 200 }], { signal }, ) : Promise.resolve([]), querySkateContent( [{ kinds: [CALENDAR_EVENT_KIND], '#a': coords, limit: 200 }], { signal }, ), ]); // Fastest lookup: author pubkey → newest note timestamp. const newestByAuthor = new Map(); for (const n of notes) { const prev = newestByAuthor.get(n.pubkey) ?? 0; if (n.created_at > prev) newestByAuthor.set(n.pubkey, n.created_at); } // Crew coord → { upcoming count, newest event ts } const now = Math.floor(Date.now() / 1000); const eventStats = new Map(); for (const ev of events) { const startTag = ev.tags.find(([t]) => t === 'start')?.[1]; const start = startTag ? parseInt(startTag, 10) : 0; const crewA = ev.tags .filter(([t]) => t === 'a') .map(([, v]) => v) .find((v) => v?.startsWith(`${SKATE_CREW_KIND}:`)); if (!crewA) continue; const cur = eventStats.get(crewA) ?? { upcoming: 0, newest: 0 }; if (start >= now) cur.upcoming += 1; if (ev.created_at > cur.newest) cur.newest = ev.created_at; eventStats.set(crewA, cur); } const result = new Map(); for (const crew of list) { const coord = crewCoordinate(crew); const feedTs = Math.max( crew.createdAt, ...[crew.pubkey, ...crew.members].map((pk) => newestByAuthor.get(pk) ?? 0), ); const evStat = eventStats.get(coord); const lastActiveAt = Math.max(feedTs, evStat?.newest ?? 0) || undefined; result.set(coord, deriveActivity(lastActiveAt, evStat?.upcoming ?? 0)); } return result; }, }); }