Add fillHeight property to WidgetDefinition. When true, WidgetCard uses
a fixed height instead of max-height on the ScrollArea, allowing the
widget's internal flex layout to properly fill the container. The AI chat
widget's messages area now scrolls correctly at a fixed height instead
of awkwardly growing with content.
The widget was hardcoding 'shakespeare' as the model name, which is
not a valid model ID. Now fetches available models from the API and
uses the cheapest one as default, matching how AIChatPage works.
Uses useBlobbiCompanionData() to detect if this Blobbi is the active
floating companion (same check as BlobbiPage). When active, hides the
visual and stat wheels, showing a Footprints icon + 'Out exploring
with you' message + gradient 'Bring home' button instead.
Extract StatIndicator into a shared component (src/blobbi/ui/StatIndicator.tsx)
with size ('sm'/'md') and onClick/disabled props. Reuse it in both
BlobbiPage (display-only, size='md') and BlobbiWidget (clickable, size='sm').
The widget now shows a single row of stat wheels that double as action
buttons: clicking the hunger wheel feeds, hygiene cleans, health heals,
happiness plays, and energy toggles sleep/wake. Removes the separate
action button row entirely.
Uses the same SVG progress ring + lucide icon pattern as BlobbiPage,
scaled down to 36px circles. Shows warning/critical alert triangles
on low stats. Much more compact vertically than the horizontal bars.
- Pass useStatusReaction recipe to BlobbiStageVisual so the widget
reflects the actual health state (dizzy eyes, stink clouds, etc.)
- Increase default widget height from 280px to 350px so quick action
buttons aren't clipped by the scroll container
- Syncs companion selection with BlobbiPage (localStorage + profile.has)
- Shows projected decay stats that update every 60s
- Adds Feed, Play, Clean, and Sleep/Wake quick action buttons
- Hides actions irrelevant to the current stage (eggs can't eat/play/sleep)
- Uses the same ensureCanonical + decay + publish flow as BlobbiPage
- Buttons disable while an action is in progress
Photos widget shows the latest photo with image, author, and caption.
Music widget shows the latest track with artwork and playable controls
via the global audio player. Both scope to the user's follow list when
logged in, or the curator's follow list when logged out.
Remove resizeOnFullScreen config which caused possiblyResizeChildOfContent()
to corrupt CoordinatorLayout height on Android 16 (API 36). Upgrade plugin
from 8.0.2 to 8.0.3 which adds a SystemBars guard as additional safety.
Platform-gate setAccessoryBarVisible to iOS only (unimplemented on Android).
The visibility-change-based Android resume detection was causing more
problems than it solved. Remove the module and simplify LoginDialog and
signerWithNudge to operate without retry-on-resume behavior.
clearAttention() now resets lastAttentionTimeRef to 0 so that a
triggerAttention call immediately after a forced clear is not silently
rejected by the 1500ms cooldown guard.
Remove right-sidebar detection and multi-target chaining. The generic
reaction now fires a single attention target at the top-center of the
main content area for a random 2-6 seconds.
- Remove findRightSidebarPosition() and all sidebar scan logic
- Replace fixed 1200ms duration with random 2000-6000ms
- Compute target position at reaction start via live DOM query
and window.innerWidth/Height fallback (no stale closure)
- Cancel both pending timeouts and active attention on drag
- Remove viewport prop from hook options (no longer needed)
- Update docstrings to describe the simplified behavior
On page navigation the companion now briefly pauses and scans the
layout areas that changed. Center content is always scanned first,
followed by the right sidebar if a non-placeholder sidebar is
detected in the DOM.
Implementation:
- useRouteReaction.ts: thin orchestration hook that watches pathname,
determines changed areas, and chains triggerAttention calls via
setTimeout. Cancels on new route change, drag, or unmount.
- useBlobbiCompanion.ts: wires the new hook with existing
triggerAttention/clearAttention from useBlobbiAttention.
No changes to the attention system, state machine, gaze hook, motion
hook, or entry animation. The existing attending state and attend-ui
gaze mode handle all the visual behavior.
Includes an empty ROUTE_REACTIONS map for future per-route overrides.
Android's shouldInterceptRequest blocks a pool of ~6 IO threads, each
waiting for JS to respond via the Capacitor bridge. With 200+ files
each requiring a network round-trip to Blossom, loading is painfully
slow. iOS doesn't have this problem — WKURLSchemeHandler is async.
Split the native plugin lifecycle into create() and navigate():
- create() adds the WebView container with spinner overlay (visible)
- navigate() loads the entry URL (triggers fetch interception)
On Android, onReady downloads all manifest blobs in parallel (12
concurrent fetches) into an in-memory cache while the native
ProgressBar spinner animates. Once navigate() fires, every resolveFile
call is an instant cache hit.
On iOS/web, onReady is a no-op and navigate() fires immediately.
The fetchFromBlossom function previously tried servers sequentially for
every file request. For nsites without server tags (falling back to 3
app default servers), each of the 200+ files paid a full round-trip
penalty when the first server returned 404 before falling through.
Now tracks a module-level preferred server. Once any server successfully
serves a blob it becomes preferred and is tried first for all subsequent
requests. This means only the first file pays the discovery cost; the
rest go directly to the server that has the content.
ComposeBox and LeftSidebar avatar fallbacks only checked metadata.name,
ignoring display_name and genUserName. Now uses the same fallback chain
as ProfileCard: display_name -> name -> genUserName(pubkey). Also fixed
the getDisplayName helper in LeftSidebar to check display_name.
Redesign FeedEmptyState with a centered icon, cleaner layout, and
two actionable buttons for the follows tab: 'Discover people to
follow' linking to /packs, and 'Browse the Global feed' to switch
tabs. Other call sites are unaffected (new props are optional).
Add min-h-dvh to the Feed <main> element so it always fills at least
the viewport height. Without this, the sticky FAB (a sibling after
<main>) sits in normal flow right after the short content instead of
at the bottom of the center column.
- Hide the small pencil icon on avatar and banner until an image is
actually set (the hover overlay still shows so users can discover
the action)
- Remove the Profile Fields collapsible from the signup flow to keep
the onboarding lightweight
handlePublishProfile already skips publishing when no data is entered,
so the Skip button was redundant. A single full-width Continue button
simplifies the UI.
ThemeStep was reading customTheme?.background?.url unconditionally,
so the background persisted even after selecting a built-in theme.
Now resolves the active theme config the same way AppProvider does,
only showing the background when the active theme actually has one.
Android 16 (API 36) enforces edge-to-edge rendering unconditionally,
breaking @capacitor/status-bar's setOverlaysWebView and setBackgroundColor.
Additionally, a Chromium bug (<140) causes env(safe-area-inset-*) to report
0 in some Android WebViews.
- Replace @capacitor/status-bar with SystemBars from @capacitor/core 8+
- Enable insetsHandling: 'css' in capacitor.config.ts so the SystemBars
plugin injects --safe-area-inset-* CSS variables on Android
- Update all safe area CSS utilities and inline styles to use
var(--safe-area-inset-*, env(safe-area-inset-*, 0px)) fallback pattern
- Remove @capacitor/status-bar dependency (no longer needed)
- Use SecAddSharedWebCredential to prompt 'Save Password?' on signup
- Use ASAuthorizationPasswordProvider to restore credentials on login
- Add webcredentials:ditto.pub Associated Domains entitlement
- Deploy apple-app-site-association for domain validation
- Keep existing Chromium PasswordCredential flow as web fallback
- Add saveNsec() helper: native credential manager on iOS/Android,
file download + bonus PasswordCredential on web
- Single 'Continue' button triggers the appropriate save method per platform
Daily mission session state is now a pubkey-scoped Map instead of
localStorage. Hydrates from kind 11125 content on mount/account switch.
Completed missions are already persisted by useAwardDailyXp; intermediate
progress resets on refresh (low-impact).
Progressive enhancement using PasswordCredential (Chromium-only).
On sign-up, the nsec is offered to the browser's password manager
alongside the existing file download. The prompt appears while the
user is looking at their key on the download step. On login, stored
credentials are retrieved for one-tap login on supported browsers.
Safari/Firefox/iOS silently skip — existing flows are unchanged.
- Fridge opens as full-page blur overlay with flex-wrap food grid
and 2x2 stat icons per item (lucide icons, no boxes/borders)
- X dismiss button with strokeWidth 4, click negative space to close
- Overlay renders above navigation arrows (z-50)
- Sleeping Blobbi cannot leave bedroom (toast + gate on room change)
- Upgrade lucide-react, add arrow nudge keyframe animations
- Replace all emoji button/room icons with lucide equivalents
- Room indicator moved below Blobbi name in hero
- Touch swipe support on room shell
- Larger nav arrows (size-7/8, strokeWidth 4)
Add a 'Delete Account' pill button to the bottom of the Settings
page (Guideline 5.1.1v). Rename the Danger Zone heading in Advanced
Settings to match. Simplify the deletion dialog to a single screen:
plain-language warning, list of what gets deleted, type DELETE to
confirm, and Cancel/Delete buttons. Always broadcasts to all relays.
The underlying NIP-62 mechanism and components that render vanish
events to other users are unchanged.
- room-config.ts: room IDs, metadata, navigation helpers, default order (no hatchery)
- room-layout.ts: shared bottom bar class constant
- poop-system.ts: ephemeral poop generation/cleanup with XP reward
- ItemCarousel.tsx: single-focus item carousel with prev/next previews
- RoomActionButton.tsx: unified circular action button for room bottom bars
- Add 'room' tag to kind 11125 schema for cross-session persistence
- Barrel exports from rooms/index.ts
- item-cooldown.ts: shared singleton with per-item cooldown tracking,
subscriber system for React integration
- useItemCooldown.ts: useSyncExternalStore hook for reactive cooldown state
- Add POOP_CLEANUP_XP (5) to blobbi-xp.ts
- Export all new APIs from actions barrel
- Add progression.ts: xpToLevel, levelToXp, xpProgress, getUnlocks (pure functions, ~110 lines)
- Add missions.ts: tally/event mission types, ProfileContent parse/serialize for kind 11125 content
- Add xp/level tags to kind 11125 BlobbonautProfile schema
- Rewrite daily-missions.ts: drop completed/claimed/currentCount, use target+count/events model
- Unify hatch/evolve into single 'evolution' key in missions content
- Replace coin rewards with XP rewards throughout
- Remove explicit claim flow (completion is implicit from progress >= target)
- Rewrite tracker, hooks, and UI consumers to new data shape
- Guard against old localStorage format during migration
Nostr events are untrusted user input. Any URL extracted from event tags
or metadata must be validated before use in any context — not just
navigable hrefs, but also img src, CSS url(), and style attributes.
Changes:
- Theme events (kind 16767/36767): validate background and font URLs
through sanitizeUrl() at parse time in themeEvent.ts
- Badge definitions (kind 30009): validate image and thumb URLs through
sanitizeUrl() at parse time in parseBadgeDefinition.ts
- Font family names: sanitize with an allowlist regex before
interpolation into CSS declarations in fontLoader.ts
- Profile fields: replace weak startsWith('http://') checks with
sanitizeUrl() in ProfileRightSidebar and ProfilePage
- Community descriptions: validate extracted URLs through sanitizeUrl()
in CommunityContent.tsx
- AGENTS.md: mandate unconditional URL sanitization for all
event-sourced URLs regardless of rendering context, document CSS
injection prevention guidelines
Add a shared sanitizeUrl() utility that validates URLs are well-formed
https: before they reach href attributes, window.open(), or openUrl().
Apply sanitization across all components that render untrusted URLs:
- CalendarEventDetailPage: r-tag links
- ZapstoreAppContent: url and repository tags
- ZapstoreReleaseContent: asset url tags passed to openUrl()
- AppHandlerContent: web handler tags and metadata.website
- NsiteCard: source tag
- GitRepoCard: web tag URLs passed to openUrl()
- FileMetadataContent: url tag used in download href
- ProfilePage: metadata.website (tighten weak startsWith check)
- useUserStatus: r-tag URL
Document sanitizeUrl usage in AGENTS.md for future agent use.