Commit Graph

4447 Commits

Author SHA1 Message Date
Chad Curtis b4e42778fa Merge remote-tracking branch 'origin/main' into ditto-music-feed
# Conflicts:
#	src/components/NoteCard.tsx
2026-04-19 17:27:12 -05:00
filemon e704399c3d Fix Pandi eye detection misidentifying ears and eye patches as pupils
Pandi's 4 ear circles and 2 eye-patch circles use flat dark fills
(#1f2937, #374151) that match PUPIL_COLORS, causing the detector to
find 8 "pupils" instead of 2. This produced phantom eye groups,
broke blink/gaze targeting, and left the real pupils orphaned.

Add data-blobbi-skip attribute to the 6 non-pupil dark circles in
Pandi's SVGs, and add an early-return in isPupilElement() to respect
it. No other form has this attribute, so behavior is unchanged for
all non-Pandi adults.
2026-04-19 00:39:49 -03:00
filemon d1ae988024 Fix Catti reaction mouths rendering left-shifted instead of centered
Catti's dual cat-mouth (two mirrored Q-curve paths sharing a center
start point) caused extractMouthPositionFromElements to return only
the left half's coordinates. Every mouth generator then computed
cx=(100+82)/2=91 instead of the true center x=100.

The fix scans all Q-curve paths in the mouth section and computes the
full horizontal extent. For Catti: startX=82, endX=118, center=100.
Single-path mouths are unaffected (the extra-path loop simply doesn't
execute). Froggi's dual-path mouth already had symmetric bounds so its
result is unchanged.
2026-04-18 23:38:41 -03:00
filemon 27736c7047 Fix Catti whiskers destroyed by mouth replacement recipes
replaceMouthSection() used a global regex matching any Q-curve <path>
with a stroke attribute. Catti's whiskers are Q-curve paths that appear
after the mouth in document order, so they were matched and deleted
whenever a status reaction replaced the mouth.

The fix adds a marker-bounded replacement strategy: when a <!-- Mouth -->
comment marker exists, the regex is scoped to only the section between
that marker and the next SVG section. All 16 adult forms and the baby
SVG already have this marker. The global regex is preserved as a
fallback for SVGs without markers.
2026-04-18 23:04:55 -03:00
filemon 6f68153306 Fix sleeping Blobbi showing open eyes in Blobbies tab grid
The MoreTabContent grid rendered BlobbiStageVisual without a sleeping
recipe, so sleeping companions got the awake base SVG with open eyes.
The sleeping visual (closed clip-rects, closed-eye lines, Zzz) is
entirely recipe-driven — without the recipe the SVG renderer always
produces the awake appearance.

Pass buildSleepingRecipe() and recipeLabel='sleeping' for companions
whose state is 'sleeping' in the tab grid call site.
2026-04-18 22:19:42 -03:00
filemon 3260350377 Merge branch 'main' into feat/blobbi-click-overstimulation-reaction 2026-04-18 20:23:04 -03:00
filemon 03d174e5cc Add one-shot eye-open animation on wake-up
When Blobbi transitions from sleeping to awake, the eyes now visibly
open over 400ms with an ease-in-out curve before the normal blink/gaze
loop resumes. The wake-up animation mirrors the sleep-entry animation:

- runWakeUpAnimation() queries fresh DOM elements from containerRef
- Sets clip-rects to the closed position (using BLINK_CLOSED_AMOUNT)
- Animates from closed to open, then calls onComplete
- The normal awake animation loop only starts after completion
- This prevents two rAF loops from fighting over clip-rect attributes

The animation only fires on genuine sleeping→awake transitions via
wasSleepingRef, not on mount or refresh when Blobbi is already awake.
If cancelled mid-animation (e.g. quick sleep re-toggle), the onComplete
callback is still invoked so the hook does not get stuck.
2026-04-18 19:26:24 -03:00
Chad Curtis f14316f024 Send reply events to tagged users' inbox relays (NIP-65)
When publishing kind 1 or kind 1111 reply events, also deliver them to
the read (inbox) relays of p-tagged users. This follows the NIP-65
recommendation that clients send events to the read relays of each
tagged user so recipients are more likely to see replies.

The inbox delivery is fire-and-forget after the main publish succeeds,
so it does not slow down the UI or block the publish flow.
2026-04-18 16:42:48 -05:00
filemon 399a3586b2 Add one-shot eye-close animation on sleep entry
When Blobbi transitions from awake to sleeping, the eyes now visibly
close over 400ms with an ease-in-out curve before settling into the
stable sleeping state. The closed-eye lines fade in during the last
40% of the animation.

The animation is driven by a standalone rAF loop in runSleepEntryAnimation()
that queries fresh DOM elements from containerRef.current — never from
stale cached refs. A wasSleepingRef tracks the previous isSleeping value
so the animation only fires on genuine awake→sleep transitions, not on
mount or refresh when Blobbi is already sleeping.

At t=1 the DOM exactly matches the recipe's static closed state, so
there is no visual discontinuity when the animation completes.
2026-04-18 18:36:17 -03:00
filemon 3bba781f49 Fix open-eye flash on first sleep transition
Clear stale cached DOM refs in useBlobbiEyes when entering sleep.
The awake animation loop caches blink/gaze SVG elements, but
dangerouslySetInnerHTML replaces the entire SVG when the sleeping
recipe is applied. The old refs' open-eye clip geometry was being
used to querySelector into the new sleeping SVG and reset the
clip-paths back to the open position, causing both open eyes and
closed-eye lines to render simultaneously.

The sleeping recipe already sets clip rects to the closed position
in the SVG string, so no JS-side clip-path reset is needed.
Clearing the caches prevents stale operations and lets fresh
caching happen naturally when Blobbi wakes up.
2026-04-18 18:09:12 -03:00
sam 1ffa5289ba legacy aliases 2026-04-18 19:35:21 +05:45
sam 6d51f6eeac geo chat wip 2026-04-18 19:12:13 +05:45
sam bd6eb18022 WORLD++ 2026-04-18 18:40:02 +05:45
filemon 0618a1ca13 Skip redundant evolution persist when content is already up-to-date
The debounce hook was re-publishing a kind 31124 event with identical
evolution content 5s after every interaction, because the primary
write path already persisted the same data inline. Now compares the
serialized content against the fresh event before publishing and
skips when they match. The hook still fires for the one case where
it is genuinely needed: event-based backfill from Nostr queries.
2026-04-18 09:33:06 -03:00
filemon 3fe1256381 Harden per-Blobbi evolution persistence across interaction paths
Five targeted fixes to the evolution mission persistence flow:

1. Inline evolution content into interaction write paths: all three
   action hooks (direct action, inventory item, companion item use) now
   read the updated evolution from the session store and embed it into
   the 31124 content in the same publish. The debounce hook remains as
   a safety net for event-based backfill, not the primary persistence.

2. Scope ensuredRef per Blobbi: the 'ensure missions exist' guard in
   useHatchTasks and useEvolveTasks was a plain boolean ref that would
   not re-run when switching companions. Now keyed by pubkey:d.

3. Add companionD to query keys: hatch-tasks and evolve-tasks queries
   were keyed by pubkey only, causing stale cache reuse across Blobbis.

4. Filter persist hook by d-tag: usePersistEvolutionProgress now checks
   detail.d against companionD so it only reacts to evolution updates
   for the active companion.

5. Clear evolution in switch mode: when switching incubation from one
   Blobbi to another, the stopped Blobbi's 31124 content now has its
   evolution[] cleared, and its session store entry is removed.
2026-04-18 09:23:08 -03:00
filemon 1bce67d21d Move Blobbi progression missions from kind 11125 to kind 31124
Evolution/hatch mission progress was stored in the shared Blobbonaut
profile (kind 11125) content JSON, causing split-brain state between
the per-user profile and per-Blobbi events. After reload, progress
could disappear or get overwritten across Blobbis because the session
store was keyed by pubkey only and persisted via a debounced write to
the wrong event.

Now:
- Daily missions remain on kind 11125 (per-user, correct)
- Evolution missions live on kind 31124 content JSON (per-Blobbi)
- Session store split: daily keyed by pubkey, evolution by pubkey:d
- usePersistEvolutionProgress writes to 31124 instead of 11125
- useHatchTasks/useEvolveTasks read from companion.evolution
- serializeProfileContent strips legacy evolution from 11125
- Start/stop incubation/evolution seed/clear 31124 content directly
- Interaction tallies pass companion d-tag for per-Blobbi tracking
2026-04-18 08:59:19 -03:00
sam 5f2e88c0f3 old pathos/agora map 2026-04-18 17:17:22 +05:45
filemon 9b9abaa855 Fix stale references to state_started_at in progression migration cleanup
- StartIncubationDialog: display progressionState instead of state in
  restart dialog text (was showing 'active' instead of 'incubating')
- blobbi-tag-schema: update deprecated tag replacedBy and category
  comment to reference progression_started_at instead of state_started_at
- blobbi.ts: update deprecation comments for incubation_time and
  start_incubation to reference progression_started_at
2026-04-18 08:10:56 -03:00
sam 55fe82adf9 community stats 2026-04-18 16:28:41 +05:45
filemon 06b53dbc82 Fix stale comments and result types from progression-state migration
Update JSDoc comments in useBlobbiIncubation to reference
progression_state/progression_started_at instead of the deprecated
state/state_started_at. Rename stateStartedAt to progressionStartedAt
in StartIncubationResult and StartEvolutionResult interfaces.
2026-04-18 07:34:07 -03:00
filemon bf6788c141 Separate progression state from activity state to prevent evolution reset
Sleep/wake toggles overwrote 'evolving' with 'sleeping', permanently
destroying evolution progress. The root cause was using a single state
tag for two orthogonal concerns: activity (active/sleeping/hibernating)
and progression (incubating/evolving).

Introduce progression_state and progression_started_at as new tags
orthogonal to the activity state tag. Sleep, wake, and hibernation
changes now never touch progression. Parser auto-migrates legacy
events that stored progression in the state tag on read.
2026-04-18 07:03:52 -03:00
Chad Curtis 363e39d72c Harden against malformed untrusted data that crashes pages
- ExternalContentPage: new URL() on non-URL NIP-73 identifiers (isbn:,
  iso3166:) threw TypeError crashing the page; now URL is only created
  for url-type content and #-prefixed strings are used for other types
- ExternalContentPage & RelayPage: decodeURIComponent on malformed
  percent-encoded URL params threw URIError; now wrapped in try/catch
- useMastodonPost: new URL() on invalid URLs inside queryFn now returns
  null instead of letting the error propagate
- colorUtils/themeEvent: malformed hex color values from theme events
  produced NaN CSS variables; added isValidHex guard in parseColorTags
2026-04-18 04:50:34 -05:00
filemon e2ce575b25 Separate progression state from activity state to prevent evolution reset
Sleep/wake toggles overwrote 'evolving' with 'sleeping', permanently
destroying evolution progress. The root cause was using a single state
tag for two orthogonal concerns: activity (active/sleeping/hibernating)
and progression (incubating/evolving).

Introduce progression_state and progression_started_at as new tags
orthogonal to the activity state tag. Sleep, wake, and hibernation
changes now never touch progression. Parser auto-migrates legacy
events that stored progression in the state tag on read.
2026-04-18 06:46:17 -03:00
Chad Curtis 36373400f8 Fix crash on invalid blurhash strings from Nostr events
Validate blurhash hashes before passing them to react-blurhash's
<Blurhash> component, which throws when the encoded length doesn't
match the component count header. Malformed hashes from third-party
events (e.g. length 92 instead of 94) now gracefully fall back to a
skeleton placeholder instead of crashing the page.
2026-04-18 04:39:14 -05:00
filemon e12d8eebdd Fix shake reaction: remove debug bypass, preserve SMIL eyes, stack shakes
- Remove temporary `true ||` debug bypasses that made nausea trigger
  regardless of hunger stat. Nausea now correctly requires hunger >= 90.
- Track `cycleHadNausea` so the recipe resolver uses a consistent
  nauseated face recipe for the entire reaction cycle, even after the
  green fill drains to 0. This prevents a structural SVG rebuild
  mid-reaction that killed SMIL spiral eye animations.
- Make shake reactions additive: starting a new shake during dizzy or
  recovering no longer resets the reaction. Instead, the phase
  transitions back to shaking, nausea level can only rise (max of
  current and new), and the dizzy hold timer extends.

Regression-of: 91de4f80
2026-04-18 05:32:36 -03:00
sam 711a9527e9 left menu rearranging 2026-04-18 14:11:07 +05:45
filemon 91de4f80d8 Add shake-to-dizzy reaction with nausea fill and fix SMIL animation stability
Shake reaction system
---------------------
Introduce a reusable shake detection + reaction pipeline that triggers
dizzy visuals when Blobbi is shaken during drag, with progressive green
nausea body fill when hunger is high (>= 90, currently debug-bypassed).

Architecture follows the same phase/level/profile pattern as click
overstimulation for future extensibility (personality variants,
additional physical-stress reactions).

New files:
- shakeDetection.ts: pure motion sampling (direction reversals,
  speed accumulation, energy integral)
- useShakeReaction.ts: 4-phase state machine (idle → shaking → dizzy
  → recovering), profile system, recipe resolution with nausea fill

Phases:
- idle:       no shake reaction active
- shaking:    user actively shaking (dizzy face + live green fill rise)
- dizzy:      post-release hold (3-8s scaled by intensity, fill drains)
- recovering: nausea draining via rAF, then back to idle

SMIL animation stability fix
-----------------------------
The nausea fill level changes ~12×/sec during drain, creating a new
recipe object each tick. This broke the React.memo barrier on
MemoizedBlobbiVisual (reference equality), triggering full SVG DOM
replacement via dangerouslySetInnerHTML — killing all SMIL animations
(dizzy spirals, sleepy blinks) on every update.

Fix: both SvgRenderers now compute a structural recipe fingerprint
that clones the recipe and strips only bodyEffects.angerRise.level.
The customizedSvg useMemo depends on this string (compared by value),
so level-only changes skip the SVG rebuild. The fill level is applied
imperatively via gradient stop setAttribute() in a separate useEffect,
preserving the existing DOM and all running SMIL animations.

Reaction retrigger fix
----------------------
Both shake and overstimulation reactions had cycle-scoped refs that
were not cleaned up when the reaction drained to idle via the natural
rAF path, preventing immediate retrigger:
- Overstimulation: clicksRef (stale timestamps) now cleared at idle
- Shake: toastShownRef now reset at idle

Body fill improvements
----------------------
- angerRise generator now accepts caller-controlled bottomOpacity and
  edgeOpacity so nausea (strong green) and anger (moderate red) can
  have different visual intensity through the same shared generator
- Static-level fill mode uses real body bounds from path detection
  instead of hardcoded coordinates
- Nausea fill drains during the dizzy hold (not only after it ends)
  for a smooth continuous descent

Temporary debug bypass still active:
  true || hungerRef.current >= _NAUSEA_HUNGER_THRESHOLD
Must be removed before final merge.
2026-04-18 04:58:10 -03:00
sam ced5d00163 ported actions/challenges 2026-04-18 13:38:50 +05:45
sam b6dffa9828 organisers, country content, external content 2026-04-18 13:03:02 +05:45
sam 5a94ef10d7 verified follow packs 2026-04-18 11:45:10 +05:45
sam ec9f57476d ++ 2026-04-18 11:44:46 +05:45
sam 6a60612ba6 poll compose 2026-04-18 11:44:31 +05:45
filemon c2af41c7f2 Simplify blocked toast to show duration once without live countdown
Revert the interval/update-in-place approach and replace with a single
toast call that displays the chosen blocked duration (e.g. 'calm down
for 3s'). No interval, no toast handle refs, no countdown state.

Removed from previous version:
- toastHandleRef and countdownIntervalRef refs
- clearCountdown() helper
- setInterval countdown loop with toast.update() calls
- clearCountdown() calls in block-end, deactivation, and unmount paths

Regression-of: 6d9e7502
2026-04-18 02:00:05 -03:00
filemon 6d9e750251 Add live countdown to Blobbi overstimulation toast
Show remaining seconds in the blocked toast (e.g. 'calm down for 4s')
and update it every second via the toast update mechanism. The toast is
automatically dismissed when the blocked phase ends. All timer state is
owned by useOverstimulationReaction so the UI layer stays simple.
2026-04-18 01:53:19 -03:00
filemon 12c19ac4c2 Block all pointer interactions during Blobbi overstimulation phase
Add a transparent fullscreen overlay (fixed, inset 0, z-index 99999) that
renders only while phase === 'blocked'. This prevents clicks on buttons,
links, inputs, feeds, menus, etc. while Blobbi is overwhelmed. The overlay
is removed automatically when the blocked phase ends.
2026-04-18 01:47:32 -03:00
filemon 7768588dbd Fix documentation to match actual overstimulation math
Update the module doc comment and inline constant comments to accurately
describe the real escalation and cooling timelines:
- 4 clicks: mild angry face
- 6 clicks: red body fill begins (level crosses 0.2)
- 15 clicks: max level, blocked for 2-4s
- 1.5s delay + ~4s drain = ~5.5s total recovery from max
2026-04-18 01:07:25 -03:00
filemon 0f759de671 Fix overstimulation reaction: phase transition ownership and angerRise level propagation
Two bugs prevented the overstimulation system from working:

1. Phase never entered 'rising' because phaseRef was mutated before
   calling pushVisible(), making the phaseChanged check always false.
   pushVisible is now the single owner of phase transitions — callers
   must not pre-mutate phaseRef. Same bug existed in the rising→cooling
   transition inside rafTick.

2. applyVisualRecipe() dropped the 'level' field when building the
   bodySpec for angerRise, so the level-controlled static gradient
   path was never reached.
2026-04-18 01:07:25 -03:00
filemon 53b0281dc8 Add click-overstimulation reaction system for Blobbi companion
Implement a profile-based overstimulation system that reacts to rapid
repeated clicks anywhere in the app. The system tracks a continuous
level (0-1) that rises with rapid clicks and cools down gradually when
clicks stop, with support for temporary click blocking at max level.

Architecture:
- useOverstimulationReaction hook with OverstimulationProfile interface
  for future personality-based branching (angry, confused, nervous, etc.)
- Level-controlled anger-rise body effect via new 'level' parameter on
  the existing angerRise spec (preserves SMIL path for existing callers)
- Visible state throttled to ~6-10fps via delta threshold to avoid
  SVG re-render churn

Behavior:
- 5+ rapid clicks in 2s window triggers mild angry face
- Additional clicks progressively fill the red body effect from bottom
- 2s of no clicks starts gradual cooling (level decreases over time)
- Clicking during cooldown resumes from current level
- At max level: Blobbi blocks clicks for 2-4s with a toast notification
- After block ends: level cools naturally from 1.0 back to idle
2026-04-18 01:06:15 -03:00
filemon f85d345821 Clean up route-reaction hook: fix stale click reuse, remove unused scaffolding, document stable callbacks 2026-04-18 01:06:15 -03:00
filemon ff44d9022c Use closest() for click-target detection, clamp Y to prevent downward gaze
Replace the manual parent-walk loop with closest('a, button,
[role="button"]') — simpler, handles role="button" elements, and
returns null cleanly when no clickable ancestor exists (so only the
raw pointer fallback is used).

Clamp the resolved click-origin Y to 55% of viewport height so Blobbi
looks across toward the sidebar rather than sharply downward when the
clicked item is near the bottom of the screen.
2026-04-18 01:06:15 -03:00
filemon cb9d183d7d Use live element rect for click-origin glance position
Store the nearest clickable ancestor (<a>/<button>) on pointerdown
alongside the raw coordinates. At route-change time, re-read the
element's getBoundingClientRect() to get its current visible center,
which accounts for any scroll shifts between the click and the React
effect. Falls back to the raw pointer coordinates if the element has
been unmounted.
2026-04-18 01:06:15 -03:00
filemon a3874a77f4 Blobbi glances at click origin before looking at main content on route change
Track the last pointerdown position in a ref. On route change, if a
recent click exists (<1s), glance at that position for ~700ms first,
then look at the center-top of the new page for 2-6s. Programmatic
navigation (no recent click) falls back to immediate center-top.

Single-file change in useRouteReaction.ts. No changes to attention,
gaze, or state hooks.
2026-04-18 01:06:15 -03:00
filemon 4264fb4aba Fix Blobbi gaze drifting away during route-reaction attention
Guard makeDecision() so it bails out while attentionTarget is active.
Previously the decision loop could fire during the attending window,
transitioning Blobbi to walking/idle and breaking the attend-ui gaze
lock. The attention-end handler already resumes decisions when it
clears, so this guard is safe.
2026-04-18 01:06:15 -03:00
filemon f2c479ea3a Eliminate sidebar-look during route transitions by setting immediate center-top attention
On route change, immediately trigger a preliminary attention target at
viewport center-top before the 250ms DOM-mount delay. This ensures the
gaze system never falls to random or mouse-follow mode during the gap,
which previously caused Blobbi to drift leftward toward the sidebar.

The delayed reaction still fires at 250ms and replaces the preliminary
target with a precise DOM-measured position.
2026-04-18 01:06:15 -03:00
filemon 7fa856224e Fix Blobbi looking at sidebar during route transitions
Keep previous attention alive during the 250ms route-reaction delay
instead of clearing it immediately. This prevents the gaze system from
falling to random mode (which could point toward the sidebar) while
waiting for the new page's DOM to mount.

- Split cancelReaction into cancelPendingTimeouts (timeouts only) and
  full cancel (timeouts + attention); route changes use the former
- Add bypassCooldown option to triggerAttention so the delayed reaction
  can override the kept-alive attention without being blocked by cooldown
- Stabilize triggerAttention via uiAttentionRef instead of stale closure
2026-04-18 01:06:15 -03:00
filemon 379c21c458 Reset attention cooldown on clearAttention to fix rapid route changes
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.
2026-04-18 01:06:15 -03:00
filemon 3b576685b7 Simplify route reaction to single center-look with random duration
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
2026-04-18 01:06:15 -03:00
filemon c95287e5a4 Add generic route-transition reaction for Blobbi companion
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.
2026-04-18 01:06:15 -03:00
Alex Gleason a2f2d9ff89 Add configurable shareOrigin to AppConfig
window.location.origin resolves to capacitor://localhost on iOS and
https://localhost on Android, which produces broken QR codes, broken
copy-link actions, and a broken remote-login callback URL on native
builds.

Add an optional shareOrigin field to AppConfig and a useShareOrigin
hook that falls back to window.location.origin when unset. Replace
all 13 call sites that build shareable URLs.

The origin can be configured three ways, in order of precedence:
user localStorage > ditto.json > VITE_SHARE_ORIGIN env var. Native
deployments can set VITE_SHARE_ORIGIN=https://ditto.pub at build time
so that shared URLs resolve correctly when opened on another device
(and get caught by DeepLinkHandler when opened on the same app via
Universal/App Links).

Regression-of: a12d5db5
2026-04-17 22:39:44 -05:00
Mary Kate Fain 800e0bbe47 Address self-review checklist findings
- Remove dead export useMusicTracksByGenre from useMusicData
- Sanitize metadata?.picture through sanitizeUrl() in ProfileCard,
  MusicHeroCard, MusicTrackCard, and MusicTrackRow
- Fix MusicDiscoverTab featured section where skeleton and loaded
  content could render simultaneously (make mutually exclusive)
- Add error state handling to all 4 music tab components
- Move genre filtering from client-side to relay-level #t filtering
  in MusicTracksTab via new genre param on useMusicFeed
2026-04-17 19:09:08 -05:00