Profile page (NIP19Page): - Add "member of crew X" summary under the header (with founder crown) - Add a full Crews sidebar card listing every crew the user founds/belongs to - Add crews count to the Contributions stats (now Spots/Shops/Crews) - Show a Bot badge for kind 0 bot accounts - CLINK "Tip in Bitcoin" already surfaces on profiles (from prior work) - New useUserCrews(pubkey) hook powering the above Messages: - Replace paste-only "New Message" dialog with NewMessageDialog: a searchable people picker (crew-mates + following, grouped, name-searchable) that also accepts a pasted npub/hex as a fallback. Starting a DM now feels like a real contacts screen. Map toolbar consistency: - Consolidate secondary add actions (Shop / Gear / Tour) into a single "+ More" popover; keep primary "Add Spot" prominent. 4 buttons -> 2 controls. Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
152 lines
4.2 KiB
TypeScript
152 lines
4.2 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';
|
|
|
|
export function useCrews(limit = 200) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
|
|
return useQuery({
|
|
queryKey: ['skate-crews', limit, getQueryKeySuffix()],
|
|
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,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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'>, limit = 30) {
|
|
const { querySkateContent, getQueryKeySuffix } = useSkateRelays();
|
|
const focusTag = CREW_FOCUS_TAG[focus];
|
|
|
|
return useQuery({
|
|
queryKey: ['skate-crews-focus', focus, limit, getQueryKeySuffix()],
|
|
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();
|
|
|
|
return useQuery({
|
|
queryKey: ['skate-crew', identifier, pubkey, getQueryKeySuffix()],
|
|
queryFn: async () => {
|
|
const signal = AbortSignal.timeout(10000);
|
|
|
|
const filter: {
|
|
kinds: number[];
|
|
'#d': string[];
|
|
authors?: string[];
|
|
limit: number;
|
|
} = {
|
|
kinds: [SKATE_CREW_KIND],
|
|
'#d': [identifier],
|
|
limit: 1,
|
|
};
|
|
if (pubkey) filter.authors = [pubkey];
|
|
|
|
const events = await querySkateContent([filter], { signal });
|
|
if (events.length === 0) return null;
|
|
|
|
const mostRecent = events.reduce((latest, current) =>
|
|
current.created_at > latest.created_at ? current : latest
|
|
);
|
|
|
|
return parseSkateCrew(mostRecent);
|
|
},
|
|
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 };
|
|
}
|