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,
|
className,
|
||||||
fallback,
|
fallback,
|
||||||
onError,
|
onError,
|
||||||
|
loading = 'lazy',
|
||||||
|
decoding = 'async',
|
||||||
...props
|
...props
|
||||||
}: ImageWithFallbackProps) {
|
}: ImageWithFallbackProps) {
|
||||||
const candidates = useMemo(() => imageCandidates(src), [src]);
|
const candidates = useMemo(() => imageCandidates(src), [src]);
|
||||||
|
|
@ -63,6 +65,8 @@ export function ImageWithFallback({
|
||||||
src={candidates[Math.min(index, candidates.length - 1)]}
|
src={candidates[Math.min(index, candidates.length - 1)]}
|
||||||
alt={alt}
|
alt={alt}
|
||||||
className={className}
|
className={className}
|
||||||
|
loading={loading}
|
||||||
|
decoding={decoding}
|
||||||
onError={(event) => {
|
onError={(event) => {
|
||||||
if (index + 1 < candidates.length) {
|
if (index + 1 < candidates.length) {
|
||||||
setIndex(index + 1);
|
setIndex(index + 1);
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,8 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
|
||||||
<img
|
<img
|
||||||
src={spot.images[0]}
|
src={spot.images[0]}
|
||||||
alt={spot.title}
|
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"
|
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 { useMyCrewInvites, useIncomingJoinRequests, useJoinRequestEndorsements } from '@/hooks/useCrewInvites';
|
||||||
import { useIncomingCollectiveJoinRequests } from '@/hooks/useCollectiveJoinRequests';
|
import { useIncomingCollectiveJoinRequests } from '@/hooks/useCollectiveJoinRequests';
|
||||||
import { useCrewDismissals } from '@/hooks/useCrewDismissals';
|
import { useCrewDismissals } from '@/hooks/useCrewDismissals';
|
||||||
import { useCrews } from '@/hooks/useCrews';
|
import { useCrews, useCrewsByMember } from '@/hooks/useCrews';
|
||||||
import { useCollectiveMembers } from '@/hooks/useCollectiveMembers';
|
import { useCollectiveMembers } from '@/hooks/useCollectiveMembers';
|
||||||
import { useSkateRelays } from '@/hooks/useSkateRelays';
|
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';
|
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: joinRequests, isLoading: joinLoading } = useIncomingJoinRequests(user?.pubkey);
|
||||||
const { data: collectiveRequestsRaw, isLoading: collectiveLoading } =
|
const { data: collectiveRequestsRaw, isLoading: collectiveLoading } =
|
||||||
useIncomingCollectiveJoinRequests(user?.pubkey);
|
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.
|
// Cross-device-synced set of dismissed invite/request ids.
|
||||||
const { data: dismissed } = useCrewDismissals();
|
const { data: dismissed } = useCrewDismissals();
|
||||||
|
|
||||||
|
|
@ -75,14 +80,14 @@ export function useCrewNotifications() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const listedCrews = useMemo(() => {
|
const listedCrews = useMemo(() => {
|
||||||
if (!user || !crews || !confirmedCoords) return [];
|
if (!user || !memberCrews || !confirmedCoords) return [];
|
||||||
return crews.filter(
|
return memberCrews.filter(
|
||||||
(c) =>
|
(c) =>
|
||||||
c.pubkey.toLowerCase() !== user.pubkey.toLowerCase() &&
|
c.pubkey.toLowerCase() !== user.pubkey.toLowerCase() &&
|
||||||
c.members.some((m) => m.toLowerCase() === user.pubkey.toLowerCase()) &&
|
c.members.some((m) => m.toLowerCase() === user.pubkey.toLowerCase()) &&
|
||||||
!confirmedCoords.has(`${SKATE_CREW_KIND}:${c.pubkey}:${c.identifier}`),
|
!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).
|
// Resolve each incoming join-request to its crew (for approval + display).
|
||||||
const requestsWithCrew = useMemo(() => {
|
const requestsWithCrew = useMemo(() => {
|
||||||
|
|
@ -144,7 +149,13 @@ export function useCrewNotifications() {
|
||||||
listedCrews,
|
listedCrews,
|
||||||
joinRequests: requestsWithEndorsements,
|
joinRequests: requestsWithEndorsements,
|
||||||
collectiveRequests,
|
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).
|
* 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
|
* 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-crews',
|
||||||
'skate-crew',
|
'skate-crew',
|
||||||
'skate-crews-focus',
|
'skate-crews-focus',
|
||||||
|
'skate-crews-member',
|
||||||
'shop-directory',
|
'shop-directory',
|
||||||
'coaches',
|
'coaches',
|
||||||
'lab-profiles',
|
'lab-profiles',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue