perf: lazy content images and targeted crew member queries
This commit is contained in:
parent
11c440b5ac
commit
075aea7f44
5 changed files with 61 additions and 6 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue