Files
eranos/src/hooks/useBlobbisCollection.ts
T
filemon ef8e9a3ccf fix: load ALL Blobbis and implement proper UI selection flow
- Update useBlobbisCollection to fetch ALL pets without limit:1
- Add chunking support (20 items per chunk) for relay compatibility
- Add debug logs for dList and 31124 query filter
- Implement localStorage-based UI selection (user-scoped key)
- Selection priority: localStorage > first in profile.has > show selector
- Add BlobbiSelectorPage for when no valid selection exists
- Add BlobbiSelectorCard component for pet selection UI
- Add 'Switch Blobbi' button in header for users with multiple pets
- Separate concerns: currentCompanion (global) vs selectedBlobbi (page UI)
2026-03-06 13:45:51 -03:00

199 lines
6.6 KiB
TypeScript

import { useCallback, useMemo } from 'react';
import { useNostr } from '@nostrify/react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { NostrEvent } from '@nostrify/nostrify';
import { useCurrentUser } from './useCurrentUser';
import {
KIND_BLOBBI_STATE,
isValidBlobbiEvent,
parseBlobbiEvent,
type BlobbiCompanion,
} from '@/lib/blobbi';
/** Maximum number of d-tags per query chunk to avoid relay issues */
const CHUNK_SIZE = 20;
/**
* Split an array into chunks of a given size.
*/
function chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
/**
* Hook to fetch ALL Blobbi companions (Kind 31124) owned by the logged-in user.
*
* Features:
* - Fetches ALL pets by d-tag list (no limit: 1)
* - Chunks large d-lists into multiple queries for relay compatibility
* - Keeps only the newest event per d-tag
* - Returns both a lookup record and array of companions
* - Provides invalidation and optimistic update helpers
*/
export function useBlobbisCollection(dList: string[] | undefined) {
const { nostr } = useNostr();
const { user } = useCurrentUser();
const queryClient = useQueryClient();
// Create a stable query key based on sorted d-tags
const sortedDList = useMemo(() => {
if (!dList || dList.length === 0) return null;
return [...dList].sort();
}, [dList]);
const queryKeyDTags = sortedDList?.join(',') ?? '';
// Main query to fetch all companions from relays
const query = useQuery({
queryKey: ['blobbi-collection', user?.pubkey, queryKeyDTags],
queryFn: async ({ signal }) => {
if (!user?.pubkey || !sortedDList || sortedDList.length === 0) {
console.log('[useBlobbisCollection] No pubkey or empty dList, returning empty');
return { companionsByD: {}, companions: [] };
}
// Log the dList we're about to query
console.log('[Blobbi] dList:', sortedDList);
// Chunk the d-list for relay compatibility
const chunks = chunkArray(sortedDList, CHUNK_SIZE);
console.log('[useBlobbisCollection] Splitting into', chunks.length, 'chunk(s)');
// Query all chunks in parallel
const allEvents: NostrEvent[] = [];
for (const chunk of chunks) {
const filter = {
kinds: [KIND_BLOBBI_STATE],
authors: [user.pubkey],
'#d': chunk,
// IMPORTANT: No limit - fetch ALL pets matching the d-tags
};
// Log the filter immediately before query
console.log('[Blobbi] 31124 query filter:', JSON.stringify(filter, null, 2));
const events = await nostr.query([filter], { signal });
allEvents.push(...events);
console.log('[useBlobbisCollection] Chunk returned', events.length, 'events');
}
console.log('[useBlobbisCollection] Total events received:', allEvents.length);
// Filter to valid events
const validEvents = allEvents.filter(isValidBlobbiEvent);
console.log('[useBlobbisCollection] Valid events:', validEvents.length);
// Group events by d-tag and keep only the newest per d
const eventsByD = new Map<string, NostrEvent>();
for (const event of validEvents) {
const dTag = event.tags.find(([name]) => name === 'd')?.[1];
if (!dTag) continue;
const existing = eventsByD.get(dTag);
if (!existing || event.created_at > existing.created_at) {
eventsByD.set(dTag, event);
}
}
// Parse all events into BlobbiCompanion objects
const companionsByD: Record<string, BlobbiCompanion> = {};
const companions: BlobbiCompanion[] = [];
for (const [dTag, event] of eventsByD) {
const parsed = parseBlobbiEvent(event);
if (parsed) {
companionsByD[dTag] = parsed;
companions.push(parsed);
}
}
console.log('[useBlobbisCollection] Parsed companions:', {
count: companions.length,
dTags: Object.keys(companionsByD),
});
return { companionsByD, companions };
},
enabled: !!user?.pubkey && !!sortedDList && sortedDList.length > 0,
staleTime: 30_000, // 30 seconds
gcTime: 5 * 60 * 1000, // 5 minutes
refetchOnWindowFocus: false,
refetchOnReconnect: true,
retry: 3,
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});
// Helper to invalidate and refetch after publishing
const invalidate = useCallback(() => {
if (user?.pubkey && queryKeyDTags) {
queryClient.invalidateQueries({
queryKey: ['blobbi-collection', user.pubkey, queryKeyDTags],
});
}
}, [queryClient, user?.pubkey, queryKeyDTags]);
// Update a single companion event in the query cache (optimistic update)
const updateCompanionEvent = useCallback((event: NostrEvent) => {
const parsed = parseBlobbiEvent(event);
if (!parsed || !user?.pubkey) return;
queryClient.setQueryData<{ companionsByD: Record<string, BlobbiCompanion>; companions: BlobbiCompanion[] }>(
['blobbi-collection', user.pubkey, queryKeyDTags],
(prev) => {
if (!prev) {
return {
companionsByD: { [parsed.d]: parsed },
companions: [parsed],
};
}
// Update the specific companion in the record
const newCompanionsByD = {
...prev.companionsByD,
[parsed.d]: parsed,
};
// Rebuild companions array from the record
const newCompanions = Object.values(newCompanionsByD);
return {
companionsByD: newCompanionsByD,
companions: newCompanions,
};
}
);
}, [queryClient, user?.pubkey, queryKeyDTags]);
// Memoize return values for stability
const companionsByD = query.data?.companionsByD ?? {};
const companions = query.data?.companions ?? [];
return {
/** Record of companions keyed by d-tag */
companionsByD,
/** Array of all companions (newest per d-tag) */
companions,
/** True only when query is loading and no data available */
isLoading: query.isLoading,
/** True when actively fetching */
isFetching: query.isFetching,
/** True when data is stale */
isStale: query.isStale,
/** Query error if any */
error: query.error,
/** Invalidate and refetch the collection */
invalidate,
/** Optimistically update a single companion in the cache */
updateCompanionEvent,
};
}