- Add 'boring' persistent face: low-energy, unamused expression (replaces sad as generic fallback)
- Droopy mouth with shallow curve, flat eyebrows
- Used for non-critical bad stats (health, happiness)
- Add 'dirty' persistent state: hygiene-specific visuals
- Includes dirt marks on lower body (3 curved scratch-like lines)
- Animated stink clouds floating upward
- Uses boring face as base + hygiene effects
- Refactor 'sleepy' to be an overlay animation
- KEY FIX: sleepy now animates the CURRENT mouth state instead of resetting to default smile
- When Blobbi is unwell (boring/dirty/dizzy face), sleepy animation preserves that base face
- Implementation: applySleepyMouthAnimation finds existing mouth path and animates from there
- Example: boring face + sleepy = boring expression with sleepy animation on top
- Update status-reactions.ts emotion mapping
- health: boring (not feeling good) → dizzy (critical)
- hygiene: dirty (poor hygiene visuals)
- happiness: boring (low energy, unamused)
- energy: sleepy (now an overlay, not base-replacing)
- Add base + overlay emotion architecture to visual components
- BlobbiAdultVisual and BlobbiBabyVisual now accept optional baseEmotion prop
- Emotions applied sequentially: base first, then overlay
- Preserves existing behavior when only one emotion provided
- Add resolveStatusEmotions() utility
- Separates base emotions from overlay emotions
- Returns StatusEmotionResult with baseEmotion and overlayEmotion
- Enables proper multi-stat handling (e.g., low health + low energy = boring face with sleepy overlay)
Architecture notes:
- Base emotions (boring, dirty, dizzy, sad, happy, etc.): replace face completely
- Overlay emotions (sleepy): animate on top without replacing base
- Critical fix: Blobbi no longer visually resets to happy during sleepy cycle when in bad state
Refactored useStatusReaction hook to be more stateful and animation-aware:
- Track currently active reaction to avoid restarting same reaction
- Distinguish between persistent (sleepy, sad, dizzy, hungry) and one-shot reactions
- Persistent reactions loop continuously while condition remains active
- Only replace reactions when: type changes, higher priority interrupts, or one-shot completes
- Remove stats from useEffect dependencies to prevent reset on every recomputation
- Add animation cycle duration awareness to avoid mid-animation interruptions
- Use refs for stats/timing to maintain stable callback references
This ensures:
- Sleepy animation completes full cycle including slow eye opening
- Crying/sad reactions don't reset before tear cycle completes
- Dizzy animation doesn't keep resetting its visual motion
- Eyebrow/face reactions don't flicker from repeated reapplication
- Standardize data attributes: data-cx/cy → data-eye-cx/cy with legacy fallback
- Replace hardcoded eye selectors with EYE_CLASSES constants from eyes/types
- Remove unused side-specific clip rect class variants from EYE_CLASSES
- Fix sleepy animation: skip JS blink when SMIL animations present
- Fix companion reaction support: skip eye transforms when CSS animations active
- Update detection.ts to try new attribute format first, fall back to legacy
- Update useBlobbiEyes and useExternalEyeOffset to respect CSS animations
window.open() and target="_blank" silently fail inside WKWebView on
iOS. Replace all programmatic window.open() calls with the openUrl()
utility from src/lib/downloadFile.ts, which uses the native share sheet
on Capacitor and falls back to window.open() on web.
Fixed in: ZapDialog, TasksPanel, HatchTasksPanel, PullRequestCard,
CustomNipCard, GitRepoCard, PatchCard.
The middle-click handler in useOpenPost is left as-is since middle-click
is a web-only interaction with no equivalent on mobile.
- Add PrivacyInfo.xcprivacy declaring UserDefaults, file timestamp, and
disk space API usage reasons, plus collected data types for crash
reporting (Sentry) and analytics (Plausible)
- Add NSPhotoLibraryUsageDescription and NSMicrophoneUsageDescription to
Info.plist for image uploads and voice message recording
Both are required for App Store submission.
Enable the floating compose button on the photos page with a camera
icon. Clicking it opens a new PhotoComposeModal with image upload,
title, caption, alt text, and content warning support. Publishes
kind 20 picture events per the NIP-68 specification.
The ProfileTabEditModal reset form state inside a handleOpenChange
callback, but Radix Dialog does not fire onOpenChange when opened
programmatically via the `open` prop. This meant that when a parent
component set `open={true}` (e.g. after clicking 'Add custom tab'),
the reset logic never ran and the form kept stale values from the
last edit session.
Replace the handleOpenChange reset with a useEffect that triggers
whenever `open` transitions to true, ensuring the form always
initializes from the current `tab` prop (or clean defaults for a
new tab).
Closes#196
Wrap badge cards in BadgeAwardNotification and BadgeAwardNotificationGroup
with Link components that navigate to the badge detail page via naddr1
encoded URLs. Both single and grouped badge notifications now link to
the badge definition page when clicked.
Closes#201
Replace the flat h-1 indicator bar in SortableTabChip with the
SubHeaderBar arc-based active indicator, matching how TabButton works.
The flat bar was overlapping the ArcBackground border stroke, creating
a visible double line when editing profile tabs.
Replace 36 MB of MP3 files with 4.6 MB of M4A (AAC-LC) files encoded
at 32kbps mono. M4A is required for iOS/Safari compatibility in
Capacitor's WKWebView.
Enable R8 minification and resource shrinking in the Android release
build to further reduce APK size. Add ProGuard rules to keep Capacitor
and OkHttp classes.
Add defense-in-depth sanitization at the output boundary of the Blobbi
SVG rendering pipeline. The upstream pipeline validates user inputs
(normalizeHexColor, instanceId regex), but 3000+ lines of regex-based
SVG string manipulation feed directly into dangerouslySetInnerHTML with
no structural guarantee that the output is safe.
sanitizeBlobbiSvg() uses DOMPurify with an allowlist tuned for the
Blobbi pipeline (gradients, clip paths, animations, @keyframes) while
blocking scripts, event handlers, foreignObject, href, and other
dangerous constructs.
- New posts flushed from the stream buffer now briefly highlight with a
primary-tinted fade animation so users can see what appeared
- New-posts pill uses responsive CSS (new-posts-pill utility) so it sits
correctly below the SubHeaderBar on both mobile and desktop
- SubHeaderBar desktop padding moved inside the inner wrapper so the arc
background extends to the viewport edge, eliminating the gap above tabs
The button only called window.scrollTo() and relied on the scroll event
listener to auto-flush the stream buffer. This failed when smooth
scrolling didn't fire reliable scroll events (especially on mobile/
Capacitor WebView). Now explicitly calls flushStreamBuffer() on click.
Encrypt and decrypt operations now call the signer directly without
nudge/timeout/retry wrapping. The sign operation already provides
the user-facing nudge when approval is needed, so the encrypt nudge
was redundant noise. Phase-transition toast and related constants
removed as dead code.
The header had bg-background/85 plus the ArcBackground SVG fill-background/85
stacking to ~98% opacity. Now the safe-area padding zone gets a single-layer
bg-background/85 fill div (same pattern as pinned SubHeaderBar), and the
ArcBackground provides the only fill for the content area.
- Decrypt operations now bypass signerWithNudge entirely (no nudge toast)
- Pinned SubHeaderBar safe area uses a separate fill div matching
MobileTopBar's bg-background/85, avoiding double-opacity stacking
with the ArcBackground SVG below
- Pinned SubHeaderBar uses safe-area-inset-top (top offset) instead of
safe-area-top (padding) so the arc stays flush with the tab content
- Throttle signer nudge toasts (8s cooldown) to prevent rapid-fire storm
when relay connection is unstable
- New posts pill fades out when nav hides instead of translating, avoiding
it floating in the safe area zone
- Signer toasts use finite duration (120s) so Radix swipe-to-dismiss works
- Lower toast swipe threshold from 50px to 30px for easier dismissal
- SubHeaderBar pinned mode adds safe-area-top padding when nav is hidden
- Increase signer nudge delay to 10s for decrypt ops (reduces false triggers on Amber)
- Add arc overhang spacer to Search, Notifications, and Profile pages
- Add PageHeader to Search page for consistent top-level layout
- Buffer streamed posts when user is scrolled down to prevent scroll jumps
- Show 'N new posts' pill that tracks SubHeaderBar position and nav state
- MediaCollage respects NIP-36 content warnings (blur/hide/show policy)
- Normalize main element classes across Profile and Notifications pages
- Use PageHeader + SubHeaderBar arc format matching other feed pages
- Add arc overhang spacer for consistent feed padding
- Move NIP-11 relay info into an inline expanding panel (maxHeight transition)
- Info toggle button in PageHeader top-right corner
- Accept string | undefined in useRelayInfo hook signature
- Register useLayoutOptions({ hasSubHeader: true }) for mobile nav