ef8e9a3ccf
- 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)
199 lines
6.6 KiB
TypeScript
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,
|
|
};
|
|
}
|