Reinstatement via kind 5 deletions will be implemented in a future branch.
Removing it now eliminates an overly-broad unscoped query and a security
issue where any pubkey could reinstate banned content.
Add two-tier moderation system for hierarchical communities using kind 1984
events scoped via A tags. Authoritative bans use NIP-32 labels
([l, ban, moderation]) and require rank authority. Soft reports use standard
NIP-56 types and trigger content warnings for any valid member.
- Update NIP.md with ban/report classification, NIP-32 label schema, and
reinstatement via kind 5
- Add parseCommunityReport(), resolveCommunityModeration() to communityUtils
- Update resolveMembership() to apply moderation overlay (remove banned members)
- Update useCommunityMembers to fetch kind 1984/5 and resolve moderation
- Add CommunityModerationContext for propagating moderation state
- Add CommunityReportDialog for soft reports (NIP-56 types)
- Add BanConfirmDialog for content removal and member bans with optional reason
- Add CommunityContentWarning component for click-to-reveal reported content
- Wire moderation into NoteMoreMenu (auto-detects community context)
- Wire moderation into CommunityDetailPage (member ban buttons, feed filtering)
- Add Remove content / Ban @user menu items to NoteMoreMenu
- Remove Copy Link to Post and Mention @user from NoteMoreMenu
- Move Mute Conversation into the mute/report section
Replace the plain launch card with a Game Boy-style cartridge (using
public/cartridge.png) whose label region centers the app icon. The whole
cartridge scales as one image and reacts to the pointer with the
existing useCardTilt 3D effect.
The cartridge is tinted by the icon's dominant color: a new
useDominantColor hook samples the icon in an off-screen canvas, picks
the heaviest hue bucket, and exposes it as HSL. A mask-image layer
masked to the cartridge silhouette blends the color over the grayscale
PNG with mix-blend-mode: color, preserving the shading. Grayscale or
CORS-blocked icons fall back to the original gray cartridge.
The app name moves out from under the cartridge and into the description
card in FileMetadataContent — rendered larger and bolder above the
note's content — while WebxdcEmbed still renders its own name card when
a parent isn't providing one (e.g. the kind 1 imeta path).
Deletes the DM implementation (DMProvider, DMContext, useDMContext,
useConversationMessages, DMChatArea, DMConversationList,
DMMessagingInterface, DMStatusInfo, dmMessageStore, dmUtils,
dmConstants, the orphaned pages/Messages.tsx, and the
nostr-direct-messages skill) and removes the corresponding wrapper
from the provider tree in App.tsx.
The feature was already disabled (dmConfig.enabled = false), so this
removes no user-visible functionality -- only ~1,600 lines in
DMProvider and the associated UI/context/hooks. The nip44/nip04 signer
paths used by drafts, letters, mute lists, and encrypted settings are
unrelated and remain. Kind 1222 voice messages are a public-feed
feature and stay.
Documentation cleanup: strip the three DM mentions from AGENTS.md
(Project Structure, App.tsx provider list, Specialized Workflows skill
pointer) and the Private Messaging bullet from README.md's feature
list. Historical CHANGELOG entries are preserved.
Replace inline arrow functions with useCallback, matching the pattern
already used in CareBar. This keeps the onFocusChange prop identity
stable across renders so ItemCarousel's internal prev/next callbacks
are not needlessly recreated.
- ItemCarousel: add effect to realign index when initialItemId changes
after mount (e.g. Blobbi switch triggers useLocalStorage key change)
- CareBar: add effect to sync focusedMeta when storedFocusId changes,
so contextual side buttons match the restored carousel item
- CareBar: remove eslint-disable on handleFocusChange by adding
setStoredFocusId to the dependency array
- Use logged-in user pubkey (useCurrentUser) instead of
companion.event.pubkey for all localStorage keys, making them
explicitly user-scoped
Remember the last user-selected room per Blobbi, and the last focused
carousel item per room. Both survive page refresh and room switching.
Room persistence:
- storedRoom written only on user-driven navigation (not sleep override)
- Sleeping temporarily forces 'rest'; waking returns to the stored room
- Falls back to profile.room tag then DEFAULT_INITIAL_ROOM
Carousel persistence:
- New initialItemId prop on ItemCarousel seeds the focused index on mount
- Each bar (Home, Kitchen, Care) stores its focused item id in localStorage
- Stale ids safely fall back to the first item (existing clamp logic)
- CareBar also initialises focusedMeta from the stored item so side
buttons render correctly on mount
Wire up pointerdown/up and the glare overlay so press-and-drag drives
the tilt on touch, matching the BadgeDetailContent behaviour. A quick
tap still opens the lightbox via the inner button. touch-action: pan-y
keeps vertical page scrolling working.
gatherer.wizards.com URLs (e.g. /BNG/en-us/156/xenagos-god-of-revels or
the legacy ?multiverseid=...) are now resolved through the Scryfall API
and rendered as actual Magic: The Gathering cards throughout the app:
- /i/<gatherer-url>: GathererCardHeader shows the card art at 280px max
width with properly rounded corners, a mouse-driven 3D tilt + specular
glare matching the badge showcase, a click-to-open lightbox, and a
face toggle for DFC/MDFC/split cards.
- Page <title> on /i/<gatherer-url> uses the real card name.
- 'Commenting on …' breadcrumbs under kind 1111 comments show the card
name with the CardsIcon and a hover-card preview.
- Parent context on PostDetailPage (e.g. /nevent1… for a comment rooted
on a Gatherer URL) shows a compact preview row matching the ISBN and
country patterns: small card art, 'Magic Card' label, card name, set.
Scryfall integration is centralised in src/lib/scryfall.ts (image URLs
and typed JSON fetching) and src/hooks/useScryfallCard.ts. MagicDeckContent
has been refactored to use the shared image helper. All four call sites
share a single TanStack Query cache keyed on the lookup, so one card
triggers one network request.
Adds special handling for Wikidata entity URLs (https://www.wikidata.org/entity/ID
and https://www.wikidata.org/wiki/ID) on the /i/ external content page.
When a Wikidata URL is used, the entity's enwiki sitelink is resolved via the
Wikidata Action API and the page renders the same rich Wikipedia embed that
would appear for the Wikipedia URL directly. Falls back to a generic link
preview when the entity has no English Wikipedia article.
Delete unused components that were superseded by inline UI in BlobbiPage:
- shop: BlobbiInventoryModal, BlobbiShopModal, BlobbiPurchaseDialog, BlobbiShopItemRow
- actions: SingModal, BlobbiMissionsModal, TasksPanel, DailyMissionsPanel,
HatchTasksPanel, StartIncubationDialog, StartEvolutionDialog
Clean up related barrel exports in actions/index.ts, stale JSDoc
in ItemEffectDisplay.tsx, a stale comment in BlobbiPage.tsx, and
an unnecessary export on findGuideItemForStat.
No behavior, UX, balance, mutation, decay, or query changes.
Extract maybeOverfeedPoop() helper so both handleFeedItem (tap/fridge)
and handleFeedFromDrag (drag-to-feed) run the same overfeed threshold
and random poop roll. Previously drag-to-feed bypassed the check
entirely, meaning overfeed poop could never trigger via drag.
Also fix orphaned JSDoc in generators.ts: move CHEW_CYCLE_SEC and its
docblock above the generateChewingMouth function so each symbol's
JSDoc attaches to the correct declaration.
Without a key, React recycles the CrumbBurst instance when a second
feed fires within the 1200ms crumb window. This prevents CSS
animations from restarting and the reward word from re-randomizing.
Using feedSeqRef.current as the key forces a full re-mount on each
new feed sequence.
The eating mouth used hardcoded rx=6/ry=7 in absolute SVG units, which
looked correct for babies (100x100 viewBox) but was proportionally too
small for adults (200x200 viewBox) and absurdly tiny for Froggi (110px
mouth width). The chewing mouth used controlY directly as the center,
which overshoots downward on deep smiles (Froggi: 12.5px too low).
New shared helper computeActionMouthGeometry(mouth) derives:
- cy from 55% of the baseline→controlY curve (visual midpoint)
- rx from clamp(halfWidth * 0.18, 4, 9) (proportional, not explosive)
- eating/chewing ry values from rx (consistent transition)
Both generateEatingMouth and generateChewingMouth now use this helper,
so the eating→chewing transition preserves the same anchor and size.
Both emit data-blobbi-mouth for DOM-based crumb positioning.
The eating recipe switches from roundMouth to the new eatingMouth flag.
Crumb spawning in BlobbiPage is now wrapped in requestAnimationFrame so
the DOM query sees the committed chewing mouth rather than the previous
eating/neutral mouth (React 18 batches setState synchronously).
Regression-of: 6903712b
The crumb origin was hardcoded at 67% of the visual wrapper height,
which misaligned with variants whose mouths sit higher (leafy 50%,
rosey 53%) or lower (froggi 72.5%, mushie 76.5%).
generateChewingMouth now:
- Emits data-blobbi-mouth="1" on the ellipse for DOM querying
- Scales rx from the detected mouth width (clamped 4-14) so wide-
mouthed variants like Froggi get a proportional chomp
BlobbiPage crumb spawning now:
- Queries [data-blobbi-mouth] inside [data-blobbi-visual]
- Uses its bounding rect center for both crumbX and crumbY
- Falls back to the old 67% ratio when the marker is absent (Owli)
- Reward text remains anchored via the visual rect (unchanged)
Crumbs now originate from per-particle sx/sy offsets across a compact
16px-wide mouth strip with slight vertical curve, then tumble mostly
downward with reduced lateral spread. Particle sizes shrunk to 2-4px
for a crumb feel. CSS keyframe no longer teleports crumbs sideways at
frame 0 — they start at their spawn point and drift to (dx, dy).
Also expanded REWARD_WORDS from 4 to 10 cute variations.
Reduce dx/dy values across all three rings so the burst stays close to
the chewing mouth instead of spraying across the face. Particle count,
colors, delays, sizes, and all other feed-reward logic are unchanged.
Extend the chewing phase (700ms → 1200ms) and crumb burst so the feed
reward feels more satisfying. Crumbs go from 6 to 12 particles in warm
amber/orange/yellow tones with wider spread and a longer CSS animation
(600ms → 1100ms). A random floating word (yum!/nom!/mmm!) drifts up
from the mouth during the burst.
All durations are now named constants (CHEW_DURATION_MS, CRUMB_DURATION_MS,
HAPPY_DURATION_MS) for easy tuning.
Both crumb and reward-pop animations respect prefers-reduced-motion.
Mutation, Nostr publishing, XP, decays, streaks remain untouched.
Drag-to-feed:
- Press a food item in the Kitchen carousel to start dragging.
- A ghost emoji follows the pointer via direct DOM mutation (no React
re-renders during pointermove).
- Near Blobbi's mouth the ghost scales down and Blobbi opens its mouth.
- Drop near the mouth to feed; drop anywhere else to cancel.
- The drag lifecycle is owned by global window listeners (pointermove,
pointerup, pointercancel, blur), not by the carousel button. This
eliminates ghost-stuck and ghost-reappear bugs caused by React
re-renders swapping button-level handlers mid-capture.
- Every listener checks pointerId + monotonic session counter.
- Cleanup is idempotent and runs on feed/miss/cancel/blur/unmount.
Chewing animation (Phase 2 polish):
- On successful feed, Blobbi shows a 700ms chewing animation (SMIL
mouth oscillation) with a crumb particle burst at the mouth.
- The item-use mutation fires immediately on drop — no delay.
- After the chewing phase, if the mutation succeeded, Blobbi shows a
happy expression for 1500ms before returning to status-based state.
- If the mutation fails, chewing clears without showing happy.
- feedSeqRef + mountedRef prevent stale timers/promises from writing
state after a newer sequence starts or the component unmounts.
- actionCleanupRef shared between tap-to-use and drag-to-feed paths
prevents a stale tap-to-use timer from clobbering drag-feed emotion.
Supporting changes:
- New 'chewing' emotion with generateChewingMouth() SMIL generator.
- replaceCurrentMouth regex handles animated <ellipse> elements.
- BlobbiRoomHero wrapped in React.memo to prevent SVG animation
restarts from unrelated parent re-renders.
- useStatusReaction memoizes the override recipe so the same
actionOverride produces a stable object reference.
- @keyframes crumb-fall added to index.css with reduced-motion support.
- CrumbBurst component renders 6 CSS-animated particles at the mouth.
- ItemCarousel.centerPointerHandlers reduced to { onPointerDown }.
- DEBUG_FOOD_DRAG flag for development tracing.
PoopOverlay renders all poops in every room, not filtered by
poop.room. The old comment described a room-aware filtering
strategy that was removed in an earlier commit.
The probability check was only in generateInitialPoops (mount time).
The live feeding path in KitchenBar always spawned a poop. Now both
paths use the shared OVERFEED_CHANCE constant.
PoopOverlay (passive) now renders all poops regardless of poop.room.
The room field still controls where the shovel can clean them
(InteractivePoopOverlay filters by roomId).
The overlays now filter by poop.room so the data model is respected
end-to-end: poop-system assigns a room, overlays render only poops
matching their room. Currently all poops spawn in kitchen so the
behavior is identical, but enabling multi-room spawning later only
requires changing the room assignment in poop-system.ts — no UI
changes needed.
- PoopOverlay and InteractivePoopOverlay take a roomId prop and
filter via getPoopsInRoom()
- Restore MAX_POOPS to 3 (was raised to 6 in previous commit)
- Fix indentation in generateInitialPoops
- All poops spawn with room='kitchen' (reverts random room assignment)
- PassivePoopOverlay renders poop emojis in Home, Care, and Rest rooms
as static display-only elements (no interaction)
- KitchenPoopOverlay renders interactive poops with drag hit-test refs
- Shovel button remains exclusively in the kitchen bar
- useShovelDrag simplified: no longer takes roomId parameter
- Remove room-aware toast ('Try another room'); simple 'Nothing to
clean!' message when tapping shovel with no poop
- Poops now spawn in random rooms (care, kitchen, home, rest) and each
room renders them with a draggable shovel via shared PoopOverlay +
ShovelButton components
- Extract drag-to-clean logic into useShovelDrag hook (reused by all
room bars, eliminating ~100 lines of duplicated code from KitchenBar)
- Overfeed poop is now 60% probability instead of deterministic
- Time-based poop cap raised from 3 to MAX_POOPS (6), so longer
absences produce more poops proportionally
- Shovel replaces the left action button in each room when poop is
present (e.g. replaces Photo in Home, Towel in Care); reverts
when poop is cleaned
- Room-aware toast: 'Try another room' when poop exists elsewhere
The auto-resize effect only depended on content, so switching back
from preview mode remounted the textarea without recalculating its
height. Adding previewMode to the dependency array ensures the
textarea is resized immediately after remounting.
Add dir="auto" to text content elements across articles (title,
summary, prose), all compose inputs (ComposeBox, ArticleEditor,
MilkdownEditor, PhotoComposeModal, ComposeLetterSheet, LetterEditor),
and letter view components (LetterDetailSheet, LetterCard body/closing/
signature).
- Replace toggle-mode shovel with drag-and-drop: drag the shovel icon
onto a poop to clean it (works on both desktop mouse and mobile touch)
- Shovel button is now always visible in the kitchen (no more disappearing)
- Always show 'Shovel' label (removed 'Done' toggle text)
- Show toast when tapping shovel with no poop present
- Fix invisible poops: all poops now spawn in kitchen only (previously
time-based poops randomly spawned in rooms that never rendered them)
- Spawn poop immediately when feeding a blobbi with hunger >= 95
- Add addPoop() to PoopState for reactive poop creation
- Convert RoomActionButton to forwardRef with touch/mouse event props
- Cap total poops at 4 to prevent accumulation
BABY_DECAY.energy and ADULT_DECAY.energy were objects with an unused
sleeping field (6.0 / 5.0) that was overridden by the standalone
BABY_SLEEP_ENERGY_REGEN (40.0) and ADULT_SLEEP_ENERGY_REGEN (35.0)
constants. Flatten energy to a plain number to avoid misleading readers.
No behavior change — the ternaries already used the standalone constants.
Regression-of: 4d4d8a43
All callers now pass careState. The old status prop, its default value,
and the three ternary fallback branches were dead code.
Regression-of: 08be5e99
The old threshold was replaced by the segment-model care badge in
08be5e99 but kept with a void suppression. The new companionNeedsCare
function is self-documenting; the old value adds no reference value.
Regression-of: 08be5e99
The fridge overlay was computing segment deltas from companion.stats
(persisted), while the stat rings show projected stats with decay
applied. This caused the preview to disagree with the visible UI.
Thread currentStats (from useProjectedBlobbiState) through
RoomBottomBarProps so KitchenBar uses the same decay-projected values
the user sees in the stat rings.
The rooms-based UI (KitchenBar, CareBar, HomeBar) fully replaced the old
BlobbiActionInventoryModal / BlobbiActionsModal flow. The inventoryAction
state was never set to a non-null value, so the modal could never open.
Deleted:
- BlobbiActionInventoryModal.tsx (311 lines)
- BlobbiActionsModal.tsx (201 lines)
Removed from blobbi-action-utils.ts:
- previewStatChanges (superseded by previewStatChangesWithSegments)
- previewMedicineForEgg, previewCleanForEgg, EggStatPreview
- filterInventoryByAction, FilterInventoryOptions, ResolvedInventoryItem
Removed from BlobbiPage.tsx:
- inventoryAction state, handleUseItem (modal version),
handleOpenShopFromAction, modal JSX block
Kept live:
- previewStatChangesWithSegments (used by KitchenBar fridge overlay)
- usingItemId / setUsingItemId (used by room loading indicators)
- All stage/action restriction helpers (used by live item-use hooks)
Add previewStatChangesWithSegments() helper that computes before/after
segment counts using getBlobbiStatDisplayState, so preview badges can
communicate bar changes alongside raw stat deltas.
UI updates:
- Action inventory modal: shows (+1 bar) after the raw delta
- Kitchen fridge overlay: shows compact +N▮ segment indicator
- Egg stage omits segment info (always protected/full)
Includes 13 tests covering baby, adult, egg, and edge cases.
Baby (4 segments):
- hunger -8/hr, happiness -4.5/hr, hygiene -6/hr, energy -9/hr
- health base -0.4/hr (was -0.75)
- health penalty thresholds aligned to segment boundaries:
mild at < 50 (attention), strong at < 25 (urgent)
— was < 70/40, which fired penalties in the 'okay' range
- health regen threshold lowered to 76 (baby good = 4/4 starts at 76)
— was 80
Adult (10 segments):
- hunger -5/hr, happiness -2.5/hr, hygiene -4/hr, energy -5.5/hr
- health base -0.25/hr (was -0.4)
- penalty thresholds unchanged (already align with 10-segment model)
- regen threshold unchanged at 80
Pacing:
- Baby first 'okay' stat at ~2.7hr, first 'attention' at ~5-6hr
- Adult first 'okay' stat at ~5-6hr, first 'attention' at ~7-8hr
- Growing up feels like increased resilience, not more annoyance
Sleep behaviour, item values, careState mapping, segmented UI
rendering, and Nostr persistence are unchanged. Tests updated to
cover new rates, penalty alignment, and regen threshold.
Part 1 — Segmented rings:
Replace the continuous strokeDasharray progress ring with discrete arc
segments driven by the segment display model. Baby/egg shows 4 segments,
adult shows 10. StatIndicator exports a reusable SegmentedRing component.
When the segments prop is absent, the old continuous ring renders as a
backward-compatible fallback.
Part 2 — Ring gap fix:
Switch from strokeLinecap round to butt so gaps are not consumed by cap
extensions. Increase gapDeg to 16/20 (md/sm), bump muted opacity to 0.12.
Offset the start angle by half a gap so the first gap straddles 12-o'clock,
making the ring visually centred and symmetrical.
Part 3 — Sleep modifiers:
Sleeping is now restorative instead of punitive:
- Energy regen: +40/hr (baby), +35/hr (adult) — up from +6/+5
- Hunger/happiness/hygiene: decay at 20% of awake rates
- Health base decay: pauses (0) while sleeping
- Health penalties: reduced to 25% of awake strength
Awake decay rates, health penalty thresholds, item/action values,
hibernating behaviour, and Nostr persistence are unchanged.
Adds 17 focused unit tests covering baby/adult sleeping, awake decay
baseline, and hibernating-is-not-sleeping.
Replace scattered warning/critical status checks with careState from
getBlobbiStatDisplayState in the three UI consumers:
- StatIndicator (shared): new careState prop takes precedence over
deprecated status prop; badge shown for attention/urgent, pulse for
urgent only.
- BlobbiRoomHero (inline indicator): same careState-driven logic.
- BlobbiWidget: passes careState instead of getStatStatus result.
- BlobbiPage companion selector: care badge now shows when any stat is
urgent OR two-plus stats are attention (was: any stat < 40). Eggs are
always protected so they never trigger the badge.
Old threshold constants and getStatStatus are kept — no deletions.
No changes to decay rates, sleep, items, status-reactions,
needDetection, SVG ring rendering, or Nostr persistence.