diff --git a/src/components/ImageWithFallback.tsx b/src/components/ImageWithFallback.tsx
index 3b24ae3..7fb8f22 100644
--- a/src/components/ImageWithFallback.tsx
+++ b/src/components/ImageWithFallback.tsx
@@ -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);
diff --git a/src/components/SpotCard.tsx b/src/components/SpotCard.tsx
index c40c2d3..0e5e6a3 100644
--- a/src/components/SpotCard.tsx
+++ b/src/components/SpotCard.tsx
@@ -78,6 +78,8 @@ export function SpotCard({ spot, status, community }: SpotCardProps) {
) : (
diff --git a/src/hooks/useCrewNotifications.ts b/src/hooks/useCrewNotifications.ts
index 66322fe..2d29f95 100644
--- a/src/hooks/useCrewNotifications.ts
+++ b/src/hooks/useCrewNotifications.ts
@@ -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,
};
}
diff --git a/src/hooks/useCrews.ts b/src/hooks/useCrews.ts
index d5c7153..132157d 100644
--- a/src/hooks/useCrews.ts
+++ b/src/hooks/useCrews.ts
@@ -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
diff --git a/src/lib/queryPersister.ts b/src/lib/queryPersister.ts
index 35ec48d..04bf4ab 100644
--- a/src/lib/queryPersister.ts
+++ b/src/lib/queryPersister.ts
@@ -68,6 +68,7 @@ const PERSIST_ALLOW_LIST = new Set([
'skate-crews',
'skate-crew',
'skate-crews-focus',
+ 'skate-crews-member',
'shop-directory',
'coaches',
'lab-profiles',