perf: lazy content images and targeted crew member queries

This commit is contained in:
SkateSpots 2026-10-28 13:11:10 +07:00
parent 11c440b5ac
commit 075aea7f44
5 changed files with 61 additions and 6 deletions

View file

@ -31,6 +31,8 @@ export function ImageWithFallback({
className,
fallback,
onError,
loading = 'lazy',
decoding = 'async',
...props
}: ImageWithFallbackProps) {
const candidates = useMemo(() => imageCandidates(src), [src]);
@ -63,6 +65,8 @@ export function ImageWithFallback({
src={candidates[Math.min(index, candidates.length - 1)]}
alt={alt}
className={className}
loading={loading}
decoding={decoding}
onError={(event) => {
if (index + 1 < candidates.length) {
setIndex(index + 1);

View file

@ -78,6 +78,8 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
<img
src={spot.images[0]}
alt={spot.title}
loading="lazy"
decoding="async"
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110 group-hover:brightness-110"
/>
) : (

View file

@ -4,7 +4,7 @@ import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useMyCrewInvites, useIncomingJoinRequests, useJoinRequestEndorsements } from '@/hooks/useCrewInvites';
import { useIncomingCollectiveJoinRequests } from '@/hooks/useCollectiveJoinRequests';
import { useCrewDismissals } from '@/hooks/useCrewDismissals';
import { useCrews } from '@/hooks/useCrews';
import { useCrews, useCrewsByMember } from '@/hooks/useCrews';
import { useCollectiveMembers } from '@/hooks/useCollectiveMembers';
import { useSkateRelays } from '@/hooks/useSkateRelays';
import { SKATE_CREW_KIND, MEMBERSHIP_LABEL_NAMESPACE, MEMBERSHIP_LABEL_VALUE, type CollectiveJoinRequest, type CrewEndorsement, type CrewInvite, type CrewJoinRequest, type SkateCrew } from '@/lib/types';
@ -36,7 +36,12 @@ export function useCrewNotifications() {
const { data: joinRequests, isLoading: joinLoading } = useIncomingJoinRequests(user?.pubkey);
const { data: collectiveRequestsRaw, isLoading: collectiveLoading } =
useIncomingCollectiveJoinRequests(user?.pubkey);
const { data: crews, isLoading: crewsLoading } = useCrews(200);
const { data: memberCrews, isLoading: memberCrewsLoading } = useCrewsByMember(user?.pubkey);
// Only admins with an actual incoming request need the full crew directory
// to resolve which crew it belongs to. Everyone else skips that 200-crew read.
const { data: crews, isLoading: crewsLoading } = useCrews(200, {
enabled: !!joinRequests?.length,
});
// Cross-device-synced set of dismissed invite/request ids.
const { data: dismissed } = useCrewDismissals();
@ -75,14 +80,14 @@ export function useCrewNotifications() {
});
const listedCrews = useMemo(() => {
if (!user || !crews || !confirmedCoords) return [];
return crews.filter(
if (!user || !memberCrews || !confirmedCoords) return [];
return memberCrews.filter(
(c) =>
c.pubkey.toLowerCase() !== user.pubkey.toLowerCase() &&
c.members.some((m) => m.toLowerCase() === user.pubkey.toLowerCase()) &&
!confirmedCoords.has(`${SKATE_CREW_KIND}:${c.pubkey}:${c.identifier}`),
);
}, [user, crews, confirmedCoords]);
}, [user, memberCrews, confirmedCoords]);
// Resolve each incoming join-request to its crew (for approval + display).
const requestsWithCrew = useMemo(() => {
@ -144,7 +149,13 @@ export function useCrewNotifications() {
listedCrews,
joinRequests: requestsWithEndorsements,
collectiveRequests,
isLoading: invitesLoading || joinLoading || collectiveLoading || crewsLoading || membershipLoading,
isLoading:
invitesLoading ||
joinLoading ||
collectiveLoading ||
memberCrewsLoading ||
crewsLoading ||
membershipLoading,
};
}

View file

@ -49,6 +49,43 @@ export function useCrews(limit = 200, options: { includeHidden?: boolean; enable
});
}
/**
* 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

View file

@ -68,6 +68,7 @@ const PERSIST_ALLOW_LIST = new Set<string>([
'skate-crews',
'skate-crew',
'skate-crews-focus',
'skate-crews-member',
'shop-directory',
'coaches',
'lab-profiles',