The volume slider was being clipped by the card's overflow:hidden.
Now using Popover component (Radix UI) which renders via portal,
ensuring the volume control appears above all UI elements correctly.
- Add restart() function to useAudioPlayback hook (sets currentTime=0 and plays)
- Replace Square icon with RotateCcw for restart semantics
- Restart button resets track to beginning and continues playing
- Stop function still exists for cleanup on close
The auto-start effect was incorrectly triggering on 'stopped' state,
causing immediate restart. Now 'stopped' is a terminal state that
requires explicit play button click to restart.
- Fix stop button: add 'stopped' state so stop truly stops playback instead of pausing at 0
- Fix track switching: detect source.url changes and reload, distinguish change vs initial selection
- Disable Upload tab in PlayMusicModal (marked 'Soon' but was still clickable)
- Remove misplaced chevron arrows from lyrics toggle button in InlineSingCard
- Move inline activity cards inside padded container to prevent overlap with fixed bottom bar
- Move audio files from src/blobbi/audio/ to public/blobbi/audio/ for correct Vite asset loading
- Update track metadata with accurate durations from ffprobe
- Update documentation comments to reflect correct asset location
- Remove unused MicOff import from InlineSingCard
Implement NIP-53 staleness heuristic (24h threshold) to mark abandoned
status=live streams as ended. Fetch all streams globally and filter
follows client-side (including p-tag participants for streaming services).
Add Live/Planned/Past pill tabs to the streams strip on /videos.
Also fix pre-existing bug where the volume icon showed unmuted while the
video was actually muted (autoplay requires muted), by syncing the
initial muted state from the media element in usePlayerControls.
Closes#88
Conditionally render the Media section and footer only when mediaEvents
is explicitly passed, so the fields-only mobile preview shows just the
profile fields without an empty Media box or the Shakespeare link.
Only pass fields to the mobile ProfileRightSidebar preview, omitting
mediaEvents and mediaLoading so the media collage section doesn't
render in the inline mobile preview.
Swap Wallet and Link pill button order. Replace the barebones mobile
preview with the real ProfileRightSidebar component (with media collage,
audio players, etc.) by adding a className prop to override the default
hidden xl:flex classes. Rename 'Sidebar Preview' to 'Profile Fields
Preview' since on mobile it's not in a sidebar.
Use the same useProfileMedia query as the profile page to fetch the
current user's media events and pass them to ProfileRightSidebar in
the settings page. The sidebar preview now shows the media collage
section just like it appears on the actual profile.
Place the pill buttons directly after the 'Profile Fields' heading so
users see the available content types immediately, before the existing
Website and Lightning fields.
Replace the hidden dropdown menu with a wrapping row of small pill-shaped
buttons, each showing the preset icon and label (Music, Photo, Video,
Email, Link, Wallet, Custom). Users can now see all available field types
at a glance and add one with a single tap. Descriptions are preserved as
hover tooltips on each pill.
Track which field index is currently uploading and replace the upload
button with a spinning Loader2 icon on that specific field row during
upload, giving immediate local feedback instead of relying on the
distant save button spinner.
Add isImageUrl and isVideoUrl helpers alongside the existing isAudioUrl.
Profile field values that point to image files now render as inline images
(clickable to open full size), and video files render using the VideoPlayer
component. Both the desktop sidebar (ProfileFieldRow) and mobile inline
view (ProfileFieldInline) support the new rendering.
Show an inline amber warning below media field inputs when the pasted URL
doesn't have the right file extension for the field type. For example,
pasting a YouTube link in a Music field warns that it won't work as an
audio player and suggests uploading a file instead. Handles three cases:
wrong type (e.g. image URL in audio field), no recognizable extension
(e.g. youtube.com), and Blossom URLs (always allowed, type can't be
determined from URL).
Replace misleading 'URL' placeholder on media fields with action-oriented
text like 'Upload audio or paste direct file link' per preset type. Update
upload button tooltips to say 'Choose file to upload'. Update Music preset
description to say 'Upload' consistently. Each preset now carries its own
valuePlaceholder through the form schema to the row component.
Use useLayoutOptions({ rightSidebar }) to render the live ProfileRightSidebar
preview in the actual app sidebar, replacing the previous approach that put
a fake two-column layout inside the center content area. The form returns
to its original max-w-xl centered layout. Mobile collapsible preview is
retained for screens below xl where the sidebar is hidden.
- Add InlineMusicPlayer component for persistent music playback UI
- Add InlineSingCard component for inline recording/lyrics experience
- Add useAudioPlayback hook for reusable audio playback logic
- Add blobbi-activity-state types for activity and reaction state management
Play Music flow:
- PlayMusicModal now serves as track picker only
- After track selection, inline player appears with play/pause/stop controls
- Action published first, playback starts only after success
Sing flow:
- Recording happens inline (no modal)
- Lyrics panel expands upward with random lyrics
- Action published only when user confirms with 'Sing for Blobbi'
Blobbi reaction state prepared for future visual animations
Replace the generic 3-item Add Field dropdown with 7 discoverable presets
(Music, Photo, Video, Email, Link, Wallet, Custom) that pre-configure
field type, default emoji labels, and scoped file-accept filters.
- Add tooltips on upload buttons showing accepted file formats per type
- Add HelpTip to Profile Fields section header with new FAQ entries
- Add live sidebar preview panel (right column on xl+, collapsible on mobile)
- Scope file picker accept attribute dynamically per field preset
- No changes to the underlying data model — presets are purely client-side UX
- Add blob: to media-src CSP directive to allow recorded audio playback
- Add robust MIME type selection helper for MediaRecorder
- Try MIME types in order: webm;opus, webm, mp4, ogg;opus, ogg
- Track actual recorder MIME type and use it for blob creation
- Add user-friendly playback error messages (non-fatal amber warnings)
- Verify PlayMusicModal blob URL handling works correctly
- Add 'Soon' badge to Upload tab in PlayMusicModal
- Relax egg-stage blocking in canUseAction() (UI visibility vs domain logic)
- Fix egg inventory filtering to only show items with egg-compatible effects
- Remove 'shell' wording from medicine UI text
- Fix canonical data usage in mutations (use canonical.companion for decay)
- Fix browser timer typing (NodeJS.Timeout -> ReturnType<typeof setInterval>)
- Fix PlayMusicModal audio source switching (recreate Audio on source change)
- Fix SingModal recording playback (track current playback URL)
- Add random lyrics helper for Sing action with collapsible UI
- Fix egg stage actions: clean and medicine now work for eggs
- Add play_music action with built-in tracks and file upload
- Add sing action with in-browser audio recording
- Hide feed/play/sleep actions for eggs in UI (not hard-blocked)
- Both new actions increase happiness only (+15/+20)
- Placeholder built-in tracks in blobbi-builtin-tracks.ts
When a theme background image is active, the rounded panel containers
were visually extending into the status bar (top) and home indicator
(bottom). Safe area insets were applied inside child elements but the
outer wrapper painted behind them. Move the insets to the outer wrapper
so the panels sit entirely within the safe area.
Replace the browser Notification constructor (tab-must-be-open) with nostr-push
Web Push notifications that work even when the browser is closed.
- Add service worker (public/sw.js) for receiving and displaying push events
- Add nostr-push RPC client (src/lib/nostrPush.ts) using an ephemeral device
keypair for signing — the user's Nostr signer is never prompted
- Add usePushNotifications hook managing the full lifecycle: SW registration,
VAPID key fetch, pushManager.subscribe, and nostr-push registration
- Replace the web path in useNativeNotifications with push registration that
auto-enables/disables based on the notificationsEnabled encrypted setting
- Android native Capacitor path is unchanged
- Requires VITE_NOSTR_PUSH_PUBKEY env var (hex pubkey of the nostr-push server)
- Add Android intent-filter with autoVerify for https://ditto.pub/*
- Create DeepLinkHandler component to handle appUrlOpen events at runtime
- Mount DeepLinkHandler in AppRouter inside BrowserRouter context
- Add public/.well-known/assetlinks.json with pub.ditto.app package and signing cert fingerprint
BREAKING: shell_integrity is fully removed from the egg model.
Eggs now use the standard 3-stat model: health, hygiene, happiness.
Changes:
- Remove EggStats, EggMedicineResult types from blobbi-action-utils.ts
- Remove applyMedicineToEgg function (medicine now uses applyStat directly)
- Update useBlobbiUseInventoryItem to apply medicine health effect to egg health
- Update BlobbiActionInventoryModal to preview health changes for egg medicine
- Remove shell_integrity from ItemEffect in shop types
- Remove shellIntegrity from BlobbiEggData in types/blobbi.ts
- Remove EggStats, EggMedicineResult, applyMedicineToEgg from exports
- Add DEPRECATED_BLOBBI_TAG_NAMES set with 'shell_integrity'
- Update mergeBlobbiStateTagsForRepublish to filter out deprecated tags
Migration: Existing events with shell_integrity tags will have them
automatically removed on the next republish (any user interaction).
Egg stat model is now fully consistent:
- health, hygiene, happiness: active (decay + medicine)
- hunger, energy: fixed at 100
- Create useBlobbiHatch hook for egg -> baby transition
- Create useBlobbiEvolve hook for baby -> adult transition
- Both hooks apply accumulated decay before publishing new state
- Wire up floating action button to trigger hatch/evolve based on stage
- Hide evolve button for adults (already fully evolved)
- Show loading state during transitions
- Export new hooks and types from blobbi/actions module
Stage transitions now consistently apply decay first, ensuring
no transition can happen from stale stats.
- Import applyBlobbiDecay in BlobbiPage
- Calculate accumulated decay before state change
- Persist decayed stats along with new sleep/wake state
- Reset last_decay_at timestamp after applying decay
This ensures stats accurately reflect elapsed time when toggling
between active and sleeping states.
Core decay system (src/lib/blobbi-decay.ts):
- Pure applyBlobbiDecay() function for deterministic decay calculation
- Stage-specific decay rates: egg (2-3hr), baby (3-5hr), adult (5-7hr)
- Health modifiers based on other stats
- Health regeneration when all stats >= 80
- Floor all deltas, clamp stats to 0-100
- Warning/critical threshold helpers
UI projection hook (src/hooks/useProjectedBlobbiState.ts):
- Calculates projected stats without publishing
- Recalculates every 60 seconds
- Returns visible stats with status indicators
BlobbiPage updates:
- Uses projected state for display
- Egg shows 3 stats (health, hygiene, happiness)
- Baby/adult shows all 5 stats
- StatIndicator supports warning/critical status styling
Mutation updates (useBlobbiUseInventoryItem):
- Applies accumulated decay before interactions
- Uses decayed stats as base for item effects
- Updates last_decay_at on every interaction
Documentation (docs/blobbi/decay-system.md):
- Comprehensive explanation of the system
- All decay rates and thresholds
- Mutation flow diagram
- Edge cases and assumptions
- egg stage: Shows Egg icon with 'Hatch' tooltip
- baby/adult stages: Shows Sparkles icon with 'Evolve' tooltip
Implementation:
- Added 'stage' prop to BlobbiDashboardFloatingControlsProps
- Created getEvolveIcon() helper - returns Egg or Sparkles based on stage
- Created getEvolveTooltip() helper - returns 'Hatch' or 'Evolve'
- Removed unused Zap import
Icon choice rationale:
- Sparkles was chosen for non-egg stages because it communicates magical
transformation, which fits the Blobbi fantasy/pet theme better than
technical icons like TrendingUp or ArrowUpCircle
New components:
- FloatingActionDef: Typed interface for button definitions
- BlobbiDashboardFloatingControls: Component that renders left and right
floating button clusters
Buttons added (all visual-only placeholders):
- Right side (top cluster):
- Settings (Settings icon)
- Set as Companion (Heart icon)
- Take a Photo (Camera icon)
- Open PiP (PictureInPicture2 icon)
- Blobbi Info (Info icon) - wired to existing info modal
- Evolve (Zap icon) - styled with accent/primary colors
- Left side:
- Back button (ArrowLeft icon) - optional, not rendered by default
Implementation:
- Uses existing QuickActionButton for consistent styling
- Evolve button has distinct accent styling (primary colors)
- Button definitions centralized in typed arrays
- Placeholder handlers use console.log('TODO: ...')
- Existing info modal functionality preserved
- BlobbiDashboard: The h2 element displaying companion.name now uses
style={{ color: companion.visualTraits.baseColor }}
- BlobbiInfoModal: The DialogTitle displaying companion.name now uses
style={{ color: companion.visualTraits.baseColor }}
- Both update correctly when selecting a different Blobbi
- Selector card names left unchanged (not prominent display)
- Removed baseColor styling from Input (was not working reliably)
- Added a separate <p> element between the input and egg visual
- The <p> displays trimmedName with style={{ color: preview.visualTraits.baseColor }}
- This element updates live as the user types (uses trimmedName from preview.name)
- Input reverted to normal theme styling (text-center font-medium)
- No duplicate name under the egg (title still not passed to EggGraphic)
- Removed the extra styled name display div (was creating a second name)
- Applied base color directly to the Input element via style prop
- Changed Input className to 'text-center font-semibold text-lg' for better visibility
- Now only ONE visible name exists: the input field itself, styled with the egg's baseColor