223 lines
6.9 KiB
TypeScript
223 lines
6.9 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
import { useMemo } from 'react';
|
|
import {
|
|
SKATE_CREW_KIND,
|
|
parseSkateCrew,
|
|
CREW_FOCUS_TAG,
|
|
type SkateCrew,
|
|
type CrewFocus,
|
|
} from '@/lib/types';
|
|
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
|
import { useAdminModeration, filterAdminHidden } from '@/hooks/useAdminModeration';
|
|
|
|
export function useCrews(limit = 200, options: { includeHidden?: boolean; enabled?: boolean } = {}) {
|
|
const { includeHidden = false, enabled = true } = options;
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
const hidden = useAdminModeration();
|
|
|
|
return useQuery({
|
|
queryKey: ['skate-crews', limit, getQueryKeySuffix()],
|
|
enabled,
|
|
// `includeHidden`: admin tooling only (review queue + undo).
|
|
select: (crews: SkateCrew[]) =>
|
|
includeHidden ? crews : filterAdminHidden(crews, SKATE_CREW_KIND, hidden),
|
|
queryFn: async () => {
|
|
const signal = AbortSignal.timeout(10000);
|
|
|
|
const events = await querySkateContent(
|
|
[
|
|
{
|
|
kinds: [SKATE_CREW_KIND],
|
|
'#t': ['skatecrew'],
|
|
limit,
|
|
},
|
|
],
|
|
{ signal }
|
|
);
|
|
|
|
const crews: SkateCrew[] = [];
|
|
for (const event of events) {
|
|
const crew = parseSkateCrew(event);
|
|
if (crew) crews.push(crew);
|
|
}
|
|
|
|
return crews.sort((a, b) => b.createdAt - a.createdAt);
|
|
},
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: 30 * 60 * 1000,
|
|
refetchOnMount: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Crews that list `pubkey` as a member. One tiny `#p` query instead of pulling
|
|
* every crew just to find the few where this user may need to confirm membership.
|
|
*/
|
|
export function useCrewsByMember(pubkey?: string) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
const hidden = useAdminModeration();
|
|
|
|
return useQuery({
|
|
queryKey: ['skate-crews-member', pubkey, getQueryKeySuffix()],
|
|
enabled: !!pubkey,
|
|
select: (crews: SkateCrew[]) => filterAdminHidden(crews, SKATE_CREW_KIND, hidden),
|
|
queryFn: async () => {
|
|
if (!pubkey) return [];
|
|
const signal = AbortSignal.timeout(8000);
|
|
const events = await querySkateContent(
|
|
[
|
|
{
|
|
kinds: [SKATE_CREW_KIND],
|
|
'#p': [pubkey],
|
|
'#t': ['skatecrew'],
|
|
limit: 100,
|
|
},
|
|
],
|
|
{ signal },
|
|
);
|
|
const crews: SkateCrew[] = [];
|
|
for (const event of events) {
|
|
const crew = parseSkateCrew(event);
|
|
if (crew) crews.push(crew);
|
|
}
|
|
return crews.sort((a, b) => b.createdAt - a.createdAt);
|
|
},
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Query crews that belong to a specific Lab scene (builder / photo / coaching).
|
|
* Filters at the relay level via the crew-focus `t` tag, so The Lab pages can
|
|
* show a "Crews in this scene" strip without scanning every crew.
|
|
*/
|
|
export function useCrewsByFocus(focus: Exclude<CrewFocus, 'mixed'> | null, limit = 30) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
const focusTag = focus ? CREW_FOCUS_TAG[focus] : '';
|
|
const hidden = useAdminModeration();
|
|
|
|
return useQuery({
|
|
queryKey: ['skate-crews-focus', focus, limit, getQueryKeySuffix()],
|
|
enabled: !!focus,
|
|
select: (crews: SkateCrew[]) => filterAdminHidden(crews, SKATE_CREW_KIND, hidden),
|
|
queryFn: async () => {
|
|
const signal = AbortSignal.timeout(10000);
|
|
|
|
const events = await querySkateContent(
|
|
[
|
|
{
|
|
kinds: [SKATE_CREW_KIND],
|
|
'#t': [focusTag],
|
|
limit,
|
|
},
|
|
],
|
|
{ signal }
|
|
);
|
|
|
|
const crews: SkateCrew[] = [];
|
|
for (const event of events) {
|
|
const crew = parseSkateCrew(event);
|
|
// Double-check focus after parsing (guards against loose relay matches).
|
|
if (crew && crew.focus === focus) crews.push(crew);
|
|
}
|
|
|
|
return crews.sort((a, b) => b.createdAt - a.createdAt);
|
|
},
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: 30 * 60 * 1000,
|
|
refetchOnMount: true,
|
|
});
|
|
}
|
|
|
|
export function useCrew(identifier: string, pubkey?: string) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
const hidden = useAdminModeration();
|
|
|
|
return useQuery({
|
|
queryKey: ['skate-crew', identifier, pubkey, getQueryKeySuffix()],
|
|
select: (crew: SkateCrew | null) =>
|
|
crew && hidden.has({ coord: `${SKATE_CREW_KIND}:${crew.pubkey}:${crew.identifier}`, id: crew.id })
|
|
? null
|
|
: crew,
|
|
queryFn: async () => {
|
|
const signal = AbortSignal.timeout(10000);
|
|
|
|
const pickMostRecent = (events: { created_at: number }[]) =>
|
|
events.length
|
|
? events.reduce((latest, current) =>
|
|
current.created_at > latest.created_at ? current : latest
|
|
)
|
|
: undefined;
|
|
|
|
// 1) Precise addressable lookup: kind + d-tag (+ author when known).
|
|
const filter: {
|
|
kinds: number[];
|
|
'#d': string[];
|
|
authors?: string[];
|
|
limit: number;
|
|
} = {
|
|
kinds: [SKATE_CREW_KIND],
|
|
'#d': [identifier],
|
|
limit: 1,
|
|
};
|
|
if (pubkey) filter.authors = [pubkey];
|
|
|
|
let events = await querySkateContent([filter], { signal });
|
|
|
|
// 2) Fallback: some relays don't index the `d` tag reliably (or choke on
|
|
// combined author + tag filters). If nothing came back and we know the
|
|
// author, fetch that author's crews and match the identifier locally.
|
|
if (events.length === 0 && pubkey) {
|
|
const byAuthor = await querySkateContent(
|
|
[{ kinds: [SKATE_CREW_KIND], authors: [pubkey], limit: 200 }],
|
|
{ signal }
|
|
);
|
|
events = byAuthor.filter((e) =>
|
|
e.tags.some(([name, value]) => name === 'd' && value === identifier)
|
|
);
|
|
}
|
|
|
|
const mostRecent = pickMostRecent(events);
|
|
if (!mostRecent) return null;
|
|
|
|
return parseSkateCrew(mostRecent as typeof events[number]);
|
|
},
|
|
enabled: !!identifier,
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: 30 * 60 * 1000,
|
|
refetchOnMount: true,
|
|
});
|
|
}
|
|
|
|
export interface UserCrewMembership {
|
|
crew: SkateCrew;
|
|
/** How the user is attached to this crew. */
|
|
role: 'founder' | 'member';
|
|
}
|
|
|
|
/**
|
|
* The crews a given pubkey belongs to — as founder or listed member.
|
|
*
|
|
* Powers the "Crews" section on a user's public profile. Founder crews sort
|
|
* first, then member crews, newest first within each group.
|
|
*/
|
|
export function useUserCrews(pubkey?: string) {
|
|
const { data: crews, isLoading, error } = useCrews(200);
|
|
|
|
const memberships = useMemo<UserCrewMembership[]>(() => {
|
|
if (!pubkey || !crews) return [];
|
|
|
|
return crews
|
|
.filter((c) => c.pubkey === pubkey || c.members.includes(pubkey))
|
|
.map((crew) => ({
|
|
crew,
|
|
role: crew.pubkey === pubkey ? ('founder' as const) : ('member' as const),
|
|
}))
|
|
.sort((a, b) => {
|
|
if (a.role !== b.role) return a.role === 'founder' ? -1 : 1;
|
|
return b.crew.createdAt - a.crew.createdAt;
|
|
});
|
|
}, [pubkey, crews]);
|
|
|
|
return { memberships, isLoading, error };
|
|
}
|