Critical safety fix: 9 out of 12 Kind 11125 publish paths were
hardcoding content: '' which would silently erase any structured
content (daily missions data) stored in the profile event.
Every write path that updates an existing profile now preserves
the existing event content via profile.event.content instead of
overwriting with empty string.
Fixed locations:
- useBlobbiOnboarding.ts: reroll preview (line 378) and adopt (476)
- BlobbiHatchingCeremony.tsx: add egg to has[] (302) and mark
onboarding done (501)
- useBlobbiMigration.ts: legacy migration (190)
- useBlobbonautProfileNormalization.ts: tag normalization (85)
- BlobbiPage.tsx: auto-fix onboardingDone (547) and companion
toggle (1183)
- useBlobbiPurchaseItem.ts: shop purchase (89)
The only two paths that still use content: '' are initial profile
creation flows (useBlobbiOnboarding auto-create and
BlobbiHatchingCeremony silent setup) where no previous event exists.
The useClaimMissionReward path already uses mergeProfileContent
for proper read-modify-write of the content JSON.
Major persistence migration for the daily missions system:
Foundation:
- New blobbonaut-content.ts defines BlobbonautProfileContent type with
dailyMissions shape, plus parseProfileContent/mergeProfileContent for
safe read-modify-write of Kind 11125 JSON content
- BlobbonautProfile type extended with parsed content field
- parseBlobbonautEvent now parses event.content as structured JSON
(empty string and invalid JSON handled gracefully)
Reward system changed from coins to XP:
- DailyMissionsState.totalCoinsEarned renamed to totalXpEarned
- All mission reward values rebalanced for XP (10-50 XP range)
- Bonus mission reward: 80 coins -> 50 XP
- useClaimMissionReward now awards XP to the active companion's
experience tag (Kind 31124) instead of coins to profile
- XP award uses fetchFreshEvent for safe read-modify-write
- XP failure is non-fatal (mission claim still succeeds)
- UI labels updated: 'coins' -> 'XP' throughout
Profile content persistence (Kind 11125):
- useClaimMissionReward now persists mission state to profile content
JSON via mergeProfileContent, preserving unrelated content fields
- Fresh profile fetched before each write to avoid overwriting
concurrent changes from other tabs/devices
- Tags preserved unchanged during content-only updates
Backward compatibility:
- localStorage readMissionsState handles legacy totalCoinsEarned field
by migrating it to totalXpEarned on read
- Daily reset logic in tracker, missions hook, and reroll hook all
handle legacy field gracefully
- Profile content parsing tolerates empty, malformed, or partial JSON
Files changed:
- New: src/blobbi/core/lib/blobbonaut-content.ts
- Modified: blobbi.ts, BlobbiPage.tsx, useClaimMissionReward.ts,
useDailyMissions.ts, useRerollMission.ts, daily-mission-tracker.ts,
daily-missions.ts, BlobbiMissionsModal.tsx, DailyMissionsPanel.tsx
Replace the local-shakespeare.dev preview domain with iframe.diy, which
provides a service-worker based sandbox. This brings the nsite preview
implementation in line with Shakespeare's approach.
Key changes:
- iframe.diy handshake: listen for 'ready', respond with 'init'
- Derive private HMAC-SHA256 subdomains via deriveIframeSubdomain('nsite', ...)
- Inject preview script into HTML responses for console forwarding,
SPA navigation tracking, and /index.html path normalization
- Remove sandbox attribute (iframe.diy manages its own sandboxing)
- Serve injected script from virtual /__injected__/preview.js path
Stat crown fix — root cause and solution:
The heroWidth measurement used a useRef + useEffect([], ...) pattern
that created a ResizeObserver once on mount. When switching rooms,
BlobbiRoomHero unmounts and remounts inside the new room component,
creating a new DOM element. But the ResizeObserver was still watching
the old detached element, so heroWidth went stale (often falling to
the default 375px or reading 0 from the detached node).
Fix: replaced useRef + useEffect with a callback ref (heroCallbackRef)
that disconnects the old ResizeObserver and creates a new one every
time the ref is assigned to a new element. This ensures heroWidth is
always measured from the currently-mounted hero container.
Additionally, the arc spread and radius values were too extreme on
desktop (120/190/230 degrees, 300px radius) which made the stats
fly far apart. Now using balanced moderate values:
- Mobile: 80/110/140 degrees
- Desktop: 90/130/160 degrees
- Radius: smooth 110px → 200px interpolation based on container width
This produces a stable, moderate crown that looks the same on initial
render and after room switches, with desktop having slightly more
breathing room than mobile without being exaggerated.
Closet hidden:
Removed 'closet' from DEFAULT_ROOM_ORDER with a comment explaining
how to re-enable it. The BlobbiClosetRoom component, its type in
BlobbiRoomId, its ROOM_META entry, and its ROOM_COMPONENTS mapping
all remain intact — only the navigation sequence excludes it.
The i-tag UUID used for webxdc coordination is attacker-controlled.
Using it directly as the iframe.diy subdomain lets a malicious event
author pick a subdomain that collides with another app's origin,
gaining access to its localStorage/IndexedDB.
Introduce a persistent random seed in localStorage (ditto:seed) and
derive the subdomain as base36(HMAC-SHA256(seed, prefix|identifier)).
The prefix (e.g. "webxdc") domain-separates different use-cases.
The subdomain is stable per device+app but unpredictable to event
authors.
Carousel stability:
- Focused item container is now overflow-hidden with explicit dimensions
(w-20 h-[4.5rem] / sm:w-24 sm:h-[5.5rem]) so no content can push
the layout wider/taller
- Label uses a fixed max-width (w-16 sm:w-20) with text-center truncate
so changing items never shifts the arrows
- Preview slots also have overflow-hidden
- Empty state matches the populated carousel height
Desktop stat spacing (mobile unchanged):
- Arc spread widened to 120/190/230 degrees (was 100/160/200)
- Radius scaled up to 300px max (was 260px)
- This gives stat indicators much more breathing room on desktop
without affecting the mobile layout at all
Poop placement refinement:
- Pre-computed safe-zone positions in lower-left and lower-right
corners, avoiding the central Blobbi hero area
- Positions stored on each PoopInstance so they're stable
- Shovel mode: active state shows ring indicator around the button
and label changes to 'Done'; poop gets drop-shadow-lg when
hoverable; hover:scale-150 for clearer interaction feedback
Care room fix:
- Treat button no longer calls invalid 'interact' DirectAction;
uses a small food item from shop instead
- Both left/right slots always render the same fixed width
(RoomActionButton or w-14/w-20 spacer) so switching between
hygiene/medicine items never causes layout shift
Visual harmony:
- Closet room bottom bar now uses the same flex items-center
justify-center horizontal layout instead of a different
flex-col structure, matching the rhythm of all other rooms
Multiple polish and interaction improvements:
1. Carousel stability:
Focused item area now uses fixed dimensions (w-20 h-16 / sm:w-24 sm:h-20)
so the carousel never reflows when switching between items. Arrows stay
perfectly stable. Preview slots also have fixed w-10 h-12 dimensions.
2. Desktop stat spacing:
Arc spread widened further on desktop: 100/160/200 degrees (was 90/140/180)
with radius up to 260px (was 220px). Stats now have much more breathing
room on desktop while mobile stays unchanged.
3. Bedroom sleep button centered:
Sleep/wake button is now in the center of the bottom bar instead of
right-aligned, making the bedroom feel more focused.
4. Sleep dark overlay:
When Blobbi is sleeping, a radial-gradient dark overlay covers the
entire room shell (all rooms, not just bedroom). Uses z-20 with
pointer-events-none so controls remain usable. Scoped to the room
shell only — app header, bottom nav, and other app UI stay unaffected.
5. Renamed Bathroom → Care Room:
Room label changed from 'Bathroom' to 'Care Room' with icon changed
from bathtub to bandage emoji, since the room also contains medicine.
6. Care room conditional side actions:
ItemCarousel now supports onFocusChange callback and meta field.
When a hygiene item is focused: Towel (left) + Shower (right).
When a medicine item is focused: Lollipop/Treat (left) + empty (right).
Side buttons swap reactively as the user cycles through items.
7. Temporary local poop system:
- poop-system.ts: ephemeral generation based on hunger >= 95 (overfeed,
kitchen-only) and hours since last interaction (time-based, random room)
- Generated once on mount, no persistence
- Poop appears as floating emoji in affected rooms
- Kitchen shows a Shovel button when any poop exists anywhere
- Shovel mode: clicking poop removes it and awards 5 XP via toast
- All rooms accept RoomPoopState prop for future expansion
- BlobbiRoomContext extended with lastFeedTimestamp
Three structural layout fixes:
1. Remove 'background cut' feeling:
- Room header (label + dots) is now absolutely positioned over the
room content instead of being a separate flex block above it. This
eliminates the stacked-panels look where each section appeared to
have its own background surface.
- Hero container no longer uses overflow-hidden; uses pt-10 for
header clearance instead. The room reads as one continuous scene.
- Room shell content area fills entirely; header and nav arrows
float over it as overlays.
2. Desktop proportions tuned (mobile preserved):
- Blobbi visual reduced on desktop: size-72/size-80/size-96 ladder
(was size-80/size-[28rem]/size-[32rem])
- Stats crown arc spread widened on desktop: 90/140/180 degrees
(was 80/120/160), with radius up to 220px (was 180px). This gives
the stat indicators more breathing room on wide screens.
- Stats crown margin: mb-4 sm:mb-8 (tighter on mobile, roomier on desktop)
- Mobile stat sizing and spacing unchanged from previous pass.
3. Mobile/tablet bottom nav clearance:
- New shared ROOM_BOTTOM_BAR_CLASS constant in room-layout.ts
- On max-sidebar (below 900px), bottom bars add padding:
calc(var(--bottom-nav-height) + env(safe-area-inset-bottom) + 1rem)
- This pushes room controls above the app's fixed bottom navigation
(Feed/Search/Notifications/Profile)
- On desktop (sidebar:), normal pb-6 applies since there's no
bottom nav bar.
- All 6 rooms now use the shared class for consistent clearance.
Apply a strict CSP header to every response served from the .xdc archive
to enforce the webxdc offline sandbox. Permits same-origin, inline, eval,
wasm, data: and blob: but blocks all external network access.
Addresses layout issues where the room felt cut off and controls were
disproportionate, especially on mobile.
Key changes:
RoomActionButton — responsive sizing:
Mobile: size-14 circle, size-7 icons
Desktop: size-20 circle, size-9 icons
Tighter gap-1 and text-[10px] labels on mobile
ItemCarousel — responsive behavior:
Mobile: focused item only + compact arrows, no prev/next previews
Desktop: focused item + translucent side previews
Smaller focused item (text-4xl mobile, text-5xl desktop)
BlobbiRoomHero — reduced visual footprint:
Blobbi visual: tighter responsive scale (size-48 -> size-60 -> size-80)
Stats crown: reduced margin (mb-6 sm:mb-10 vs mb-14)
Stat indicators: size-14 sm:size-[4.5rem] (was size-[4.5rem] sm:size-20)
Glow blur reduced on mobile (-m-16 vs -m-24)
overflow-hidden to prevent hero from clipping
HomeRoom — unified bottom bar:
Removed -mt-10 negative margin hack that caused visual clipping
Photo, Carousel, and Companion now sit in one flex row with
items-center alignment, forming a single cohesive bottom composition
All rooms — standardized bottom bar pattern:
Consistent px-3 sm:px-6 pb-4 sm:pb-6 pt-1 spacing
flex items-center justify-between gap-1 sm:gap-3 layout
Spacer divs match button widths (w-14 sm:w-20) for balance
Room shell content area uses flex flex-col to ensure rooms
fill the available vertical space correctly.
Major improvements to room layout and visual consistency:
- ItemCarousel: single-focus carousel showing one main item at center
with translucent prev/next previews and left/right navigation arrows.
Replaces the horizontal scroll rows in Kitchen, Care, and Home rooms.
- RoomActionButton: unified circular button component matching the
original Photo and Companion button visual language (radial glow
background, consistent size, hover lift, label beneath).
Used in all rooms for consistent visual weight.
- BlobbiRestRoom: new dedicated bedroom for sleep/wake behavior,
with a Moon/Sun toggle as a RoomActionButton on the bottom-right.
Sleep/wake removed from Home room.
- Updated default room order: care -> kitchen -> home -> hatchery ->
rest -> closet (looped in both directions).
- All room bottom bars now use consistent px-4 sm:px-8 pb-6 spacing
with items-start justify-between layout for left/center/right zones.
- HatcheryRoom left/right buttons (Blobbis, Quests) upgraded to
RoomActionButton with badge support.
Replace the tab/drawer layout with a room-based system where each room
represents a specific area of Blobbi interaction:
- Care (bathroom): hygiene tools, medicine, towel, shower
- Kitchen: food carousel, fridge modal
- Home: toys, music, sing, photo, companion, lamp/sleep
- Hatchery: hatch/evolve progress, quests sheet, Blobbis sheet
- Closet: placeholder for future wardrobe
Architecture:
- Room types, config, and navigation helpers in src/blobbi/rooms/lib/
- BlobbiRoomShell manages current room state and left/right navigation
- BlobbiRoomHero shared component for the Blobbi visual + stats crown
- Each room is a self-contained component receiving BlobbiRoomContext
- Default room order is data-driven (DEFAULT_ROOM_ORDER array) to
support future per-user customization
- Navigation direction tracked for future animated transitions
- All existing hooks, mutations, and flows preserved unchanged
Migrate the webxdc iframe runtime from webxdc.app to iframe.diy. Instead of
sending ZIP bytes to the iframe and having the SW unzip them, the parent now
unzips the .xdc archive and serves files via iframe.diy's fetch-proxy RPC.
A webxdc bridge script is served as a virtual /webxdc.js file, and a
<script> tag is injected into HTML responses via DOMParser to load it.
- Rewrite Webxdc.tsx to use iframe.diy's ready/init/fetch protocol
- Unzip .xdc archives on the parent side and serve via fetch RPC responses
- Serve webxdc bridge as virtual /webxdc.js via the fetch handler
- Inject <script src="/webxdc.js"> into HTML using DOMParser
Portal ProfileImageLightbox to document.body, matching the fix
already applied to the shared Lightbox component. Without the
portal, the lightbox was trapped inside the center column's z-0
stacking context from MainLayout, causing the right sidebar
(a sibling outside that context) to paint on top.
Replace outdated references to 'inventory items', 'consume',
'quantity', and 'storage decrement' across 14 files. Comments
now consistently describe items as reusable abilities sourced
from the shop catalog, not consumable inventory.
Items are now single-use abilities — tap item, press Use, effect
happens immediately. No confirmation dialogs or quantity selectors.
Changes:
- Remove BlobbiUseItemConfirmDialog and InventoryUseConfirmDialog
- Remove quantity state, selectors, and multi-use loops from modals
- Simplify mutation hooks to always apply item effects once
- Drop quantity parameter from UseItemFunction type signature
- Update all call sites through the full stack (BlobbiPage, context,
companion layer, companion item use hook)
Items are now treated as abilities/tools unlocked by stage, not
consumable inventory that must be purchased. All catalog items are
shown in the companion action menu regardless of inventory quantity.
Changes:
- Source items from shop catalog instead of user inventory storage
- Remove quantity validation and storage decrement on item use
- Remove quantity badges and 'in inventory' text from all modals
- Keep stage-based filtering (egg vs baby/adult restrictions)
- Cap quantity selector at 99 instead of inventory count
Replaceable and addressable event headers now distinguish between
first publish and subsequent updates using the published_at tag:
- published_at == created_at → 'created' verb (e.g. 'created an emoji pack')
- published_at != created_at → 'updated' verb (e.g. 'updated an emoji pack')
- no published_at → 'shared' fallback for backward compatibility
Extend useNostrPublish with an optional `prev` property on the event
template. For replaceable and addressable kinds, the hook automatically
manages published_at:
- First publish (no prev): set published_at equal to created_at
- Update (prev provided): preserve published_at from the old event
- Old event lacks published_at: don't fabricate one
- Caller already set published_at in tags: leave it alone
Callers pass `prev` when they have the old event from fetchFreshEvent,
giving the hook everything it needs without extra network requests.
Updated all 11 call sites that publish replaceable or addressable events.
Documents the prev convention in AGENTS.md.
Two issues caused custom tab feeds (e.g. Magic Decks) to loop:
1. ProfileSavedFeedContent flattened pages without deduplication, so
events returned by multiple pages rendered as visible duplicates.
2. useTabFeed only stopped paginating when rawCount === 0. For
addressable events the relay keeps returning the same latest
versions, so rawCount never hit zero. Changed to rawCount < limit
(relay returned fewer than requested = exhausted).
Replace grouped-by-emoji layout with a flat list where each reaction
row shows an inline emoji badge (similar to the zap amount badge).
Add an emoji summary bar at the top when multiple emoji types are
present. This makes it immediately obvious who reacted with what.
The scroll-aware active indicator reporting and scroll listener logic was
duplicated between TabButton and SortableTabChip. Extract into a shared
useActiveTabIndicator hook in SubHeaderBar.
- Add pencil icon to SortableTabChip for editing existing custom tabs
- Wire onEdit to open ProfileTabEditModal with the existing tab data
- Clear the active arc underline when an active tab is removed (cleanup in useLayoutEffect)
- Round dnd-kit transform values to avoid sub-pixel rendering issues
SubHeaderBar: add left/right chevron scroll arrows on desktop when tabs
overflow, with gradient fade. Auto-scroll active tab into view and keep
arc hover/active indicators aligned during horizontal scroll.
ContentSettings: add Interest Tabs section with inline add/remove for
hashtags and geotags. Remove buttons always visible (mobile-friendly),
X icons use strokeWidth 4.
On desktop, overflowing feed tabs were completely inaccessible since the
scrollbar was hidden and there was no swipe gesture. Add left/right
chevron scroll buttons that appear only on desktop when tabs overflow,
with gradient fade indicators. Also auto-scrolls the active tab into
view when switching tabs, and keeps the arc hover/active indicators
aligned during horizontal scroll.
When a depth-collapsed 'Show X more replies' button was the last item
in a reply sequence, it lacked a bottom border separator. Added an
isLast prop to ExpandThreadButton that adds border-b when the button
terminates the visual sequence.
DashboardShell uses fixed positioning on mobile, placing it directly
over the body background image. Without the bg-background/85 class
that MainLayout's center column provides, the raw background image
showed through unthemed. Add the same 85% opacity background overlay
used consistently across the rest of the app.
All blobbi mutations now follow the read-modify-write pattern: fetch fresh
state from relays before mutating, then optimistically update the cache.
This prevents two classes of bugs:
1. Stale cache reads: mutations were reading from TanStack Query cache
(30s staleTime) instead of relays, causing newer events to be silently
overwritten with old stats when actions happened within the cache window.
2. Invalidation races: every mutation called invalidateCompanion() after
the optimistic update, which triggered a refetch from relays before the
just-published event had propagated, overwriting the optimistic data
with the pre-mutation state.
Changes:
- ensureCanonicalBlobbiBeforeAction now fetches fresh companion + profile
from relays (the read step) instead of using cached closure values
- useBlobbiCareActivity fetches fresh companion before streak updates
- Removed all invalidateCompanion()/invalidateProfile() calls after
optimistic updates across every action hook
- updateCompanionEvent now updates ALL blobbi-collection query caches
for the user, not just the specific d-tag list it was instantiated with,
keeping BlobbiPage and companion layer caches in sync
Settings (theme, sidebar, etc.) changed on one device were not applied
on other devices. Three root causes:
1. NostrSync seeded lastSyncedTimestamp to remoteSync on first load,
then the guard (remoteSync <= lastSyncedTimestamp) blocked the same
data from being applied. Settings were never applied on page reload.
2. The encrypted settings query had staleTime: Infinity and
refetchOnWindowFocus: false, so remote changes were never fetched.
3. useInitialSync was missing customTheme, corsProxy, faviconUrl, and
linkPreviewUrl fields.
To avoid gating every F5 behind a spinner, a lastSync timestamp is
now persisted to localStorage whenever settings are applied. On reload,
InitialSyncGate checks this: if present, render immediately from
localStorage and let NostrSync hot-swap remote changes in background.
If absent (new browser, cleared storage), show the spinner until
settings load.
Initial sync applied the theme mode (e.g. 'custom') from encrypted
settings but not the customTheme config (colors, fonts, background),
so the theme appeared broken on first login requiring manual setup
which also triggered an unwanted kind 16767 publish.
Set hasSubHeader on LetterComposePage so the MobileTopBar uses a flat
rect instead of the down-arc variant, preventing the 20px arc overhang
from painting over the LetterEditor picker panel.
Introduce a /follow/:npub deep link that auto-follows a user when
visited by a logged-in user, or presents an immersive business card
with a 'Follow on Ditto' CTA for logged-out visitors. The page applies
the target user's profile theme, renders their feed with infinite
scroll, and uses the same banner/avatar/arc styling as the main profile.
Add a FollowQRDialog that generates a themed QR code for the follow
URL. The QR colors are derived from the active theme: primary color
for modules (with contrast-safe darkening/lightening), and background
color for the QR background. Foreground text color is used when it is
colorful and offers significantly better contrast.
Surface the QR dialog from: own profile page (top-level button),
profile more menu, desktop sidebar account popover, and mobile drawer.
Custom emoji images with natural dimensions <= 16x16 now render with
image-rendering: pixelated to preserve crisp pixels instead of blurring.
Also consolidates 6 direct <img> sites to use the shared CustomEmojiImg
component so all custom emoji rendering benefits from this behavior.