Fix reactive companion visibility and add BlobbiPage duplicate prevention
Root cause: useBlobbiCompanionData had a separate query key (['companion-profile'])
from useBlobbonautProfile (['blobbonaut-profile']). When companion was
selected/removed via BlobbiPage, only the main profile query was invalidated,
leaving the companion layer with stale data.
Fix:
- Rewrote useBlobbiCompanionData to use useBlobbonautProfile instead of
duplicating the profile query
- Now shares the same query cache, so profile updates (including currentCompanion
changes) immediately propagate to the companion layer
- Added explicit null return when currentCompanionD is undefined for reactive removal
BlobbiPage duplicate prevention:
- Added check for active floating companion (isActiveFloatingCompanion)
- When the displayed Blobbi is the same as the floating companion, show a
friendly message: '{name} is out exploring right now.' with Footprints icon
- Prevents seeing two identical Blobbis (one floating, one in-page)
This makes companion selection/removal/replacement fully reactive without
requiring page refresh.
This commit is contained in:
@@ -3,6 +3,10 @@
|
||||
*
|
||||
* Fetches the current companion data from the user's Blobbonaut profile.
|
||||
* This is the data layer - it handles fetching and provides companion data.
|
||||
*
|
||||
* IMPORTANT: This hook uses useBlobbonautProfile to ensure reactivity.
|
||||
* When the profile is updated (e.g., companion selected/removed), this hook
|
||||
* automatically receives the update via the shared query cache.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
@@ -10,12 +14,9 @@ import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useBlobbonautProfile } from '@/hooks/useBlobbonautProfile';
|
||||
import {
|
||||
KIND_BLOBBONAUT_PROFILE,
|
||||
KIND_BLOBBI_STATE,
|
||||
getBlobbonautQueryDValues,
|
||||
isValidBlobbonautEvent,
|
||||
parseBlobbonautEvent,
|
||||
isValidBlobbiEvent,
|
||||
parseBlobbiEvent,
|
||||
} from '@/lib/blobbi';
|
||||
@@ -34,46 +35,27 @@ interface UseBlobbiCompanionDataResult {
|
||||
* Hook to fetch the current companion from the user's Blobbonaut profile.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Fetch the user's kind 31125 (Blobbonaut Profile) event
|
||||
* 2. Read the current_companion tag
|
||||
* 1. Use useBlobbonautProfile to get the profile (shared query, reactive)
|
||||
* 2. Read the currentCompanion from the profile
|
||||
* 3. If it exists, fetch the corresponding kind 31124 (Blobbi State) event
|
||||
* 4. Return the minimal data needed for rendering
|
||||
*
|
||||
* Reactivity:
|
||||
* - Uses the same query cache as useBlobbonautProfile
|
||||
* - When profile is updated via updateProfileEvent(), this hook reacts immediately
|
||||
* - No duplicate queries or stale cache issues
|
||||
*/
|
||||
export function useBlobbiCompanionData(): UseBlobbiCompanionDataResult {
|
||||
const { nostr } = useNostr();
|
||||
const { user } = useCurrentUser();
|
||||
|
||||
// Step 1: Fetch the Blobbonaut profile
|
||||
const profileQuery = useQuery({
|
||||
queryKey: ['companion-profile', user?.pubkey],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!user?.pubkey) return null;
|
||||
|
||||
const dValues = getBlobbonautQueryDValues(user.pubkey);
|
||||
const events = await nostr.query([{
|
||||
kinds: [KIND_BLOBBONAUT_PROFILE],
|
||||
authors: [user.pubkey],
|
||||
'#d': dValues,
|
||||
}], { signal });
|
||||
|
||||
// Get the latest valid event
|
||||
const validEvents = events
|
||||
.filter(isValidBlobbonautEvent)
|
||||
.sort((a, b) => b.created_at - a.created_at);
|
||||
|
||||
if (validEvents.length === 0) return null;
|
||||
|
||||
return parseBlobbonautEvent(validEvents[0]);
|
||||
},
|
||||
enabled: !!user?.pubkey,
|
||||
staleTime: 60_000, // 1 minute
|
||||
gcTime: 5 * 60_000, // 5 minutes
|
||||
});
|
||||
// Use the shared profile hook - this ensures reactivity when profile changes
|
||||
const { profile, isLoading: profileLoading } = useBlobbonautProfile();
|
||||
|
||||
// Extract current companion d-tag
|
||||
const currentCompanionD = profileQuery.data?.currentCompanion;
|
||||
// Extract current companion d-tag from the reactive profile
|
||||
const currentCompanionD = profile?.currentCompanion;
|
||||
|
||||
// Step 2: Fetch the Blobbi state if we have a current companion
|
||||
// Fetch the Blobbi state if we have a current companion
|
||||
const blobbiQuery = useQuery({
|
||||
queryKey: ['companion-blobbi', user?.pubkey, currentCompanionD],
|
||||
queryFn: async ({ signal }) => {
|
||||
@@ -99,8 +81,13 @@ export function useBlobbiCompanionData(): UseBlobbiCompanionDataResult {
|
||||
gcTime: 5 * 60_000, // 5 minutes
|
||||
});
|
||||
|
||||
// Step 3: Transform to CompanionData
|
||||
// Transform to CompanionData
|
||||
// When currentCompanionD becomes null/undefined, companion becomes null
|
||||
const companion = useMemo((): CompanionData | null => {
|
||||
// If no current companion is set in profile, return null immediately
|
||||
// This ensures removal is reactive
|
||||
if (!currentCompanionD) return null;
|
||||
|
||||
const blobbi = blobbiQuery.data;
|
||||
if (!blobbi) return null;
|
||||
|
||||
@@ -117,11 +104,11 @@ export function useBlobbiCompanionData(): UseBlobbiCompanionDataResult {
|
||||
adultType: blobbi.adultType,
|
||||
seed: blobbi.seed,
|
||||
};
|
||||
}, [blobbiQuery.data]);
|
||||
}, [currentCompanionD, blobbiQuery.data]);
|
||||
|
||||
return {
|
||||
companion,
|
||||
isLoading: profileQuery.isLoading || (!!currentCompanionD && blobbiQuery.isLoading),
|
||||
error: profileQuery.error ?? blobbiQuery.error ?? null,
|
||||
isLoading: profileLoading || (!!currentCompanionD && blobbiQuery.isLoading),
|
||||
error: blobbiQuery.error ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
+31
-15
@@ -27,6 +27,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogClose } from '@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { BlobbiStageVisual } from '@/blobbi/ui/BlobbiStageVisual';
|
||||
import { BlobbiPhotoModal } from '@/blobbi/ui/BlobbiPhotoModal';
|
||||
import { useBlobbiCompanionData } from '@/blobbi/companion/hooks/useBlobbiCompanionData';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
import {
|
||||
@@ -765,6 +766,11 @@ function BlobbiDashboard({
|
||||
const isSleeping = companion.state === 'sleeping';
|
||||
const isEgg = companion.stage === 'egg';
|
||||
|
||||
// Check if this Blobbi is currently the active floating companion
|
||||
// If so, we hide the visual here to avoid duplication (one floating, one in-page)
|
||||
const { companion: activeCompanion } = useBlobbiCompanionData();
|
||||
const isActiveFloatingCompanion = activeCompanion?.d === companion.d;
|
||||
|
||||
// Projected state with decay applied (UI-only, recalculates every 60s)
|
||||
const projectedState = useProjectedBlobbiState(companion);
|
||||
|
||||
@@ -1273,21 +1279,31 @@ function BlobbiDashboard({
|
||||
</div>
|
||||
|
||||
{/* Main Blobbi Visual */}
|
||||
<div className={cn(
|
||||
"relative transition-all duration-500",
|
||||
isSleeping && "opacity-80"
|
||||
)}>
|
||||
{/* Subtle glow effect behind the egg */}
|
||||
<div className="absolute inset-0 -m-8 bg-primary/5 rounded-full blur-3xl" />
|
||||
|
||||
<BlobbiStageVisual
|
||||
companion={companion}
|
||||
size="lg"
|
||||
animated={!isSleeping}
|
||||
reaction={blobbiReaction}
|
||||
className="size-48 sm:size-56"
|
||||
/>
|
||||
</div>
|
||||
{isActiveFloatingCompanion ? (
|
||||
// Show message when Blobbi is active as floating companion
|
||||
<div className="flex flex-col items-center justify-center size-48 sm:size-56 text-center">
|
||||
<Footprints className="size-12 text-muted-foreground/50 mb-3" />
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{companion.name} is out exploring right now.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className={cn(
|
||||
"relative transition-all duration-500",
|
||||
isSleeping && "opacity-80"
|
||||
)}>
|
||||
{/* Subtle glow effect behind the egg */}
|
||||
<div className="absolute inset-0 -m-8 bg-primary/5 rounded-full blur-3xl" />
|
||||
|
||||
<BlobbiStageVisual
|
||||
companion={companion}
|
||||
size="lg"
|
||||
animated={!isSleeping}
|
||||
reaction={blobbiReaction}
|
||||
className="size-48 sm:size-56"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stage Badge */}
|
||||
<Badge variant="outline" className="mt-6 capitalize">
|
||||
|
||||
Reference in New Issue
Block a user